From 8dc6137373bd337c95fee31d58a5638a099f45f2 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 26 Oct 2013 16:43:39 +0200 Subject: [PATCH 001/270] whitespace cleanup --- scour/scour.py | 5328 ++++++++++++++++++++-------------------- scour/svg_regex.py | 2 +- scour/svg_transform.py | 10 +- scour/yocto_css.py | 48 +- setup.py | 2 +- 5 files changed, 2696 insertions(+), 2694 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 491e617..7e538b1 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -58,283 +58,283 @@ from yocto_css import parseCssString # Python 2.3- did not have Decimal try: - from decimal import * + from decimal import * except ImportError: - print >>sys.stderr, "Scour requires Python 2.4." + print >>sys.stderr, "Scour requires Python 2.4." # Import Psyco if available try: - import psyco - psyco.full() + import psyco + psyco.full() except ImportError: - pass + pass APP = 'scour' VER = '0.26' COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2010' -NS = { 'SVG': 'http://www.w3.org/2000/svg', - 'XLINK': 'http://www.w3.org/1999/xlink', - 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', - 'INKSCAPE': 'http://www.inkscape.org/namespaces/inkscape', - 'ADOBE_ILLUSTRATOR': 'http://ns.adobe.com/AdobeIllustrator/10.0/', - 'ADOBE_GRAPHS': 'http://ns.adobe.com/Graphs/1.0/', - 'ADOBE_SVG_VIEWER': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', - 'ADOBE_VARIABLES': 'http://ns.adobe.com/Variables/1.0/', - 'ADOBE_SFW': 'http://ns.adobe.com/SaveForWeb/1.0/', - 'ADOBE_EXTENSIBILITY': 'http://ns.adobe.com/Extensibility/1.0/', - 'ADOBE_FLOWS': 'http://ns.adobe.com/Flows/1.0/', - 'ADOBE_IMAGE_REPLACEMENT': 'http://ns.adobe.com/ImageReplacement/1.0/', - 'ADOBE_CUSTOM': 'http://ns.adobe.com/GenericCustomNamespace/1.0/', - 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/' - } +NS = { 'SVG': 'http://www.w3.org/2000/svg', + 'XLINK': 'http://www.w3.org/1999/xlink', + 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'INKSCAPE': 'http://www.inkscape.org/namespaces/inkscape', + 'ADOBE_ILLUSTRATOR': 'http://ns.adobe.com/AdobeIllustrator/10.0/', + 'ADOBE_GRAPHS': 'http://ns.adobe.com/Graphs/1.0/', + 'ADOBE_SVG_VIEWER': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', + 'ADOBE_VARIABLES': 'http://ns.adobe.com/Variables/1.0/', + 'ADOBE_SFW': 'http://ns.adobe.com/SaveForWeb/1.0/', + 'ADOBE_EXTENSIBILITY': 'http://ns.adobe.com/Extensibility/1.0/', + 'ADOBE_FLOWS': 'http://ns.adobe.com/Flows/1.0/', + 'ADOBE_IMAGE_REPLACEMENT': 'http://ns.adobe.com/ImageReplacement/1.0/', + 'ADOBE_CUSTOM': 'http://ns.adobe.com/GenericCustomNamespace/1.0/', + 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/' + } 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['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'] ] svgAttributes = [ - 'clip-rule', - 'display', - 'fill', - 'fill-opacity', - 'fill-rule', - 'filter', - 'font-family', - 'font-size', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'line-height', - 'marker', - 'marker-end', - 'marker-mid', - 'marker-start', - 'opacity', - 'overflow', - 'stop-color', - 'stop-opacity', - 'stroke', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'visibility' - ] + 'clip-rule', + 'display', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'font-family', + 'font-size', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'line-height', + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + 'opacity', + 'overflow', + 'stop-color', + 'stop-opacity', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'visibility' + ] 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)', + } default_attributes = { # excluded all attributes with 'auto' as default - # SVG 1.1 presentation attributes - 'baseline-shift': 'baseline', - 'clip-path': 'none', - 'clip-rule': 'nonzero', - 'color': '#000', - 'color-interpolation-filters': 'linearRGB', - 'color-interpolation': 'sRGB', - 'direction': 'ltr', - 'display': 'inline', - 'enable-background': 'accumulate', - 'fill': '#000', - 'fill-opacity': '1', - 'fill-rule': 'nonzero', - 'filter': 'none', - 'flood-color': '#000', - 'flood-opacity': '1', - 'font-size-adjust': 'none', - 'font-size': 'medium', - 'font-stretch': 'normal', - 'font-style': 'normal', - 'font-variant': 'normal', - 'font-weight': 'normal', - 'glyph-orientation-horizontal': '0deg', - 'letter-spacing': 'normal', - 'lighting-color': '#fff', - 'marker': 'none', - 'marker-start': 'none', - 'marker-mid': 'none', - 'marker-end': 'none', - 'mask': 'none', - 'opacity': '1', - 'pointer-events': 'visiblePainted', - 'stop-color': '#000', - 'stop-opacity': '1', - 'stroke': 'none', - 'stroke-dasharray': 'none', - 'stroke-dashoffset': '0', - 'stroke-linecap': 'butt', - 'stroke-linejoin': 'miter', - 'stroke-miterlimit': '4', - 'stroke-opacity': '1', - 'stroke-width': '1', - 'text-anchor': 'start', - 'text-decoration': 'none', - 'unicode-bidi': 'normal', - 'visibility': 'visible', - 'word-spacing': 'normal', - 'writing-mode': 'lr-tb', - # SVG 1.2 tiny properties - 'audio-level': '1', - 'solid-color': '#000', - 'solid-opacity': '1', - 'text-align': 'start', - 'vector-effect': 'none', - 'viewport-fill': 'none', - 'viewport-fill-opacity': '1', - } + # 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) @@ -344,212 +344,212 @@ 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 +# @staticmethod + 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' +# @staticmethod + 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 + 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) + # 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) - if int(self.value) == self.value: - self.value = int(self.value) + 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)) + 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 + # 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]] + # 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)])) + # 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) + 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 + 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 ids.has_key(id) : - 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: - if ids.has_key(id) : - ids[id][0] += 1 - ids[id][1].append(node) - else: - ids[id] = [1,[node]] + global referencingProps + if prop in referencingProps and val != '' : + if len(val) >= 7 and val[0:5] == 'url(#' : + id = val[5:val.find(')')] + if ids.has_key(id) : + 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: + if ids.has_key(id) : + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] numIDsRemoved = 0 numElemsRemoved = 0 @@ -566,2640 +566,2642 @@ 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): - """ - 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.parentNode != None and goner.nodeName in removeTags: - 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.parentNode != None and goner.nodeName in removeTags: + goner.parentNode.removeChild(goner) + num += 1 + numElemsRemoved += 1 - # 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 + # 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, 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] - curIdNum = 1 + curIdNum = 1 - for rid in idList: - curId = intToID(curIdNum) - # 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) - # Then go rename it. - num += renameID(doc, rid, curId, identifiedElements, referencedIDs) - curIdNum += 1 + for rid in idList: + curId = intToID(curIdNum) + # 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) + # Then go rename it. + num += renameID(doc, rid, curId, identifiedElements, referencedIDs) + curIdNum += 1 - return num + return num def intToID(idnum): - """ - 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 rid + return 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 + definingNode = identifiedElements[idFrom] + definingNode.setAttribute("id", idTo) + del identifiedElements[idFrom] + identifiedElements[idTo] = definingNode - referringNodes = referencedIDs[idFrom] + referringNodes = referencedIDs[idFrom] - # 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 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 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 identifiedElements.keys(): - node = identifiedElements[id] - if referencedIDs.has_key(id) == 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 identifiedElements.keys(): + node = identifiedElements[id] + if referencedIDs.has_key(id) == 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 xrange(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 xrange(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) + 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 + # now recurse for children + for child in node.childNodes: + num += removeNamespacedElements(child, namespaces) + return num def removeMetadataElements(doc): - global numElemsRemoved - num = 0 - # clone the list, as the tag list is live from the DOM - elementsToRemove = [element for element in doc.documentElement.getElementsByTagName('metadata')] + global numElemsRemoved + num = 0 + # clone the list, as the tag list is live from the DOM + elementsToRemove = [element for element in doc.documentElement.getElementsByTagName('metadata')] - for element in elementsToRemove: - element.parentNode.removeChild(element) - num += 1 - numElemsRemoved += 1 + for element in elementsToRemove: + element.parentNode.removeChild(element) + num += 1 + numElemsRemoved += 1 - return num + 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 + """ + 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) + 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 + 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 + # 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 + """ + 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 + 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 + # 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 num in xrange(attrList.length): - attr = attrList.item(num) - # 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 + 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 num in xrange(attrList.length): + attr = attrList.item(num) + # 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 xrange(len(childElements)): - # skip first child - if childNum == 0: - continue + # for each subsequent child element + for childNum in xrange(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 + 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 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] + distinctAttrs = [] + # loop through all current 'common' attributes + for name in 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 commonAttrs.keys(): - for child in childElements: - child.removeAttribute(name) - elem.setAttribute(name, commonAttrs[name]) + # commonAttrs now has all the inheritable attributes which are common among all child elements + for name in 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 + # 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. + """ + 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. + 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. + If all children have a common attribute, an extra is not created. - This function acts recursively on the given element. - """ - num = 0 - global numElemsRemoved + 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) + # 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) != '': - # 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 + if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '': + # 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 + # 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 + 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. + 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 + # 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) + # each child gets the same treatment, recursively + for childNode in elem.childNodes: + if childNode.nodeType == 1: + num += createGroupsForCommonAttributes(childNode) - return num + 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 + """ + 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) + 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 + # 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 num in xrange(attrList.length): - attr = attrList.item(num) - 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 + # get all attribute values on this parent + attrList = elem.attributes + unusedAttrs = {} + for num in xrange(attrList.length): + attr = attrList.item(num) + 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 xrange(len(childElements)): - child = childElements[childNum] - inheritedAttrs = [] - for name in unusedAttrs.keys(): - val = child.getAttribute(name) - if val == '' or val == None or val == 'inherit': - inheritedAttrs.append(name) - for a in inheritedAttrs: - del unusedAttrs[a] + # for each child, if at least one child inherits the parent's attribute, then remove + for childNum in xrange(len(childElements)): + child = childElements[childNum] + inheritedAttrs = [] + for name in 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 unusedAttrs.keys(): - elem.removeAttribute(name) - num += 1 + # unusedAttrs now has all the parent attributes that are unused + for name in unusedAttrs.keys(): + elem.removeAttribute(name) + num += 1 - return num + return num def removeDuplicateGradientStops(doc): - global numElemsRemoved - num = 0 + 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)) + 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 stops.has_key(offset) : - oldStop = stops[offset] - if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: - stopsToRemove.append(stop) - stops[offset] = [color, opacity, style] + color = stop.getAttribute('stop-color') + opacity = stop.getAttribute('stop-opacity') + style = stop.getAttribute('style') + if stops.has_key(offset) : + 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 + for stop in stopsToRemove: + stop.parentNode.removeChild(stop) + num += 1 + numElemsRemoved += 1 - # linear gradients - return num + # linear gradients + return num def collapseSinglyReferencedGradients(doc): - global numElemsRemoved - num = 0 + global numElemsRemoved + num = 0 - identifiedElements = findElementsWithId(doc.documentElement) + identifiedElements = findElementsWithId(doc.documentElement) - # make sure to reset the ref'ed ids for when we are running this in testscour - for rid,nodeCount in findReferencedElements(doc.documentElement).iteritems(): - 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) + # make sure to reset the ref'ed ids for when we are running this in testscour + for rid,nodeCount in findReferencedElements(doc.documentElement).iteritems(): + 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) + # 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)) + # 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 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)) + # 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 remove the xlink:href from refElem + refElem.removeAttributeNS(NS['XLINK'], 'href') - # now delete elem - elem.parentNode.removeChild(elem) - numElemsRemoved += 1 - num += 1 - return num + # now delete elem + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + num += 1 + return num def removeDuplicateGradients(doc): - global numElemsRemoved - num = 0 + global numElemsRemoved + num = 0 - gradientsToRemove = {} - duplicateToMaster = {} + 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 + 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; + # 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 + if someGradAttrsDoNotMatch: continue - # compare xlink:href values too - if grad.getAttributeNS(NS['XLINK'], 'href') != ograd.getAttributeNS(NS['XLINK'], 'href'): - 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') + # all gradient properties match, now time to compare stops + stops = grad.getElementsByTagName('stop') + ostops = ograd.getElementsByTagName('stop') - if stops.length != ostops.length: continue + if stops.length != ostops.length: continue - # now compare stops - stopsNotEqual = False - for i in xrange(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 + # now compare stops + stopsNotEqual = False + for i in xrange(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 not gradientsToRemove.has_key(ograd): - if not duplicateToMaster.has_key(ograd): - if not gradientsToRemove.has_key(grad): - gradientsToRemove[grad] = [] - gradientsToRemove[grad].append( ograd ) - duplicateToMaster[ograd] = grad + # ograd is a duplicate of grad, we schedule it to be removed UNLESS + # ograd is ALREADY considered a 'master' element + if not gradientsToRemove.has_key(ograd): + if not duplicateToMaster.has_key(ograd): + if not gradientsToRemove.has_key(grad): + 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 gradientsToRemove.keys(): - master_id = masterGrad.getAttribute('id') -# print 'master='+master_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 - dup_id = dupGrad.getAttribute('id') -# print 'dup='+dup_id -# print referencedIDs[dup_id] - # for each element that referenced the gradient we are going to remove - for elem in referencedIDs[dup_id][1]: - # find out which attribute referenced the duplicate gradient - for attr in ['fill', 'stroke']: - v = elem.getAttribute(attr) - if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": - elem.setAttribute(attr, 'url(#'+master_id+')') - 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] - if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": - styles[style] = 'url(#'+master_id+')' - _setStyle(elem, styles) + # get a collection of all elements that are referenced and their referencing elements + referencedIDs = findReferencedElements(doc.documentElement) + for masterGrad in gradientsToRemove.keys(): + master_id = masterGrad.getAttribute('id') +# print 'master='+master_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 + dup_id = dupGrad.getAttribute('id') +# print 'dup='+dup_id +# print referencedIDs[dup_id] + # for each element that referenced the gradient we are going to remove + for elem in referencedIDs[dup_id][1]: + # find out which attribute referenced the duplicate gradient + for attr in ['fill', 'stroke']: + v = elem.getAttribute(attr) + if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": + elem.setAttribute(attr, 'url(#'+master_id+')') + 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] + if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": + styles[style] = 'url(#'+master_id+')' + _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 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 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 styleMap.has_key(prop) : - 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 styleMap.has_key(prop) : + 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 styleMap.has_key('opacity') : - 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 styleMap.has_key(uselessStyle): - del styleMap[uselessStyle] - num += 1 + # Here is where we can weed out unnecessary styles like: + # opacity:1 + if styleMap.has_key('opacity') : + 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 styleMap.has_key(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 styleMap.has_key('stroke') and styleMap['stroke'] == 'none' : - for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit', - 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'] : - if styleMap.has_key(strokestyle) : - del styleMap[strokestyle] - num += 1 - # TODO: This is actually a problem if a parent element has a specified stroke - # we need to properly calculate computed values - del styleMap['stroke'] + # 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 styleMap.has_key('stroke') and styleMap['stroke'] == 'none' : + for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit', + 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'] : + if styleMap.has_key(strokestyle) : + del styleMap[strokestyle] + num += 1 + # TODO: This is actually a problem if a parent element has a specified stroke + # we need to properly calculate computed values + del styleMap['stroke'] - # if fill:none, then remove all fill-related properties (fill-rule, etc) - if styleMap.has_key('fill') and styleMap['fill'] == 'none' : - for fillstyle in [ 'fill-rule', 'fill-opacity' ] : - if styleMap.has_key(fillstyle) : - del styleMap[fillstyle] - num += 1 + # if fill:none, then remove all fill-related properties (fill-rule, etc) + if styleMap.has_key('fill') and styleMap['fill'] == 'none' : + for fillstyle in [ 'fill-rule', 'fill-opacity' ] : + if styleMap.has_key(fillstyle) : + del styleMap[fillstyle] + num += 1 - # fill-opacity: 0 - if styleMap.has_key('fill-opacity') : - fillOpacity = float(styleMap['fill-opacity']) - if fillOpacity == 0.0 : - for uselessFillStyle in [ 'fill', 'fill-rule' ] : - if styleMap.has_key(uselessFillStyle): - del styleMap[uselessFillStyle] - num += 1 + # fill-opacity: 0 + if styleMap.has_key('fill-opacity') : + fillOpacity = float(styleMap['fill-opacity']) + if fillOpacity == 0.0 : + for uselessFillStyle in [ 'fill', 'fill-rule' ] : + if styleMap.has_key(uselessFillStyle): + del styleMap[uselessFillStyle] + num += 1 - # stroke-opacity: 0 - if styleMap.has_key('stroke-opacity') : - strokeOpacity = float(styleMap['stroke-opacity']) - if strokeOpacity == 0.0 : - for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset' ] : - if styleMap.has_key(uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 + # stroke-opacity: 0 + if styleMap.has_key('stroke-opacity') : + strokeOpacity = float(styleMap['stroke-opacity']) + if strokeOpacity == 0.0 : + for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', + 'stroke-dasharray', 'stroke-dashoffset' ] : + if styleMap.has_key(uselessStrokeStyle): + del styleMap[uselessStrokeStyle] + num += 1 - # stroke-width: 0 - if styleMap.has_key('stroke-width') : - 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 styleMap.has_key(uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 + # stroke-width: 0 + if styleMap.has_key('stroke-width') : + 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 styleMap.has_key(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 styleMap.has_key(fontstyle) : - 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 styleMap.has_key(fontstyle) : + 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 styleMap.has_key(inkscapeStyle): - 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 styleMap.has_key(inkscapeStyle): + del styleMap[inkscapeStyle] + num += 1 - if styleMap.has_key('overflow') : - # overflow specified on element other than svg, marker, pattern - if not node.nodeName in ['svg','marker','pattern']: - del styleMap['overflow'] - num += 1 - # it is a marker, pattern or svg - # as long as this node is not the document , then only - # remove overflow='hidden'. See - # http://www.w3.org/TR/2010/WD-SVG11-20100622/masking.html#OverflowProperty - elif node != node.ownerDocument.documentElement: - if styleMap['overflow'] == 'hidden': - del styleMap['overflow'] - num += 1 - # else if outer svg has a overflow="visible", we can remove it - elif styleMap['overflow'] == 'visible': - del styleMap['overflow'] - num += 1 + if styleMap.has_key('overflow') : + # overflow specified on element other than svg, marker, pattern + if not node.nodeName in ['svg','marker','pattern']: + del styleMap['overflow'] + num += 1 + # it is a marker, pattern or svg + # as long as this node is not the document , then only + # remove overflow='hidden'. See + # http://www.w3.org/TR/2010/WD-SVG11-20100622/masking.html#OverflowProperty + elif node != node.ownerDocument.documentElement: + if styleMap['overflow'] == 'hidden': + del styleMap['overflow'] + num += 1 + # else if outer svg has a overflow="visible", 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 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 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 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 def taint(taintedSet, taintedAttribute): - u"""Adds an attribute to a set of attributes. + 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 + Related attributes are also included.""" + taintedSet.add(taintedAttribute) + if taintedAttribute == 'marker': + taintedSet |= set(['marker-start', 'marker-mid', 'marker-end']) + if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']: + taintedSet.add('marker') + return taintedSet def removeDefaultAttributeValues(node, options, tainted=set()): - u"""'tainted' keeps a set of attributes defined in parent nodes. + 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 - # gradientUnits: objectBoundingBox - if node.getAttribute('gradientUnits') == 'objectBoundingBox': - node.removeAttribute('gradientUnits') - num += 1 + # gradientUnits: objectBoundingBox + if node.getAttribute('gradientUnits') == 'objectBoundingBox': + node.removeAttribute('gradientUnits') + num += 1 - # spreadMethod: pad - if node.getAttribute('spreadMethod') == 'pad': - node.removeAttribute('spreadMethod') - num += 1 + # spreadMethod: pad + if node.getAttribute('spreadMethod') == 'pad': + node.removeAttribute('spreadMethod') + num += 1 - # x1: 0% - if node.getAttribute('x1') != '': - x1 = SVGLength(node.getAttribute('x1')) - if x1.value == 0: - node.removeAttribute('x1') - num += 1 + # x1: 0% + if node.getAttribute('x1') != '': + x1 = SVGLength(node.getAttribute('x1')) + if x1.value == 0: + node.removeAttribute('x1') + num += 1 - # y1: 0% - if node.getAttribute('y1') != '': - y1 = SVGLength(node.getAttribute('y1')) - if y1.value == 0: - node.removeAttribute('y1') - num += 1 + # y1: 0% + if node.getAttribute('y1') != '': + y1 = SVGLength(node.getAttribute('y1')) + if y1.value == 0: + node.removeAttribute('y1') + num += 1 - # x2: 100% - if node.getAttribute('x2') != '': - x2 = SVGLength(node.getAttribute('x2')) - if (x2.value == 100 and x2.units == Unit.PCT) or (x2.value == 1 and x2.units == Unit.NONE): - node.removeAttribute('x2') - num += 1 + # x2: 100% + if node.getAttribute('x2') != '': + x2 = SVGLength(node.getAttribute('x2')) + if (x2.value == 100 and x2.units == Unit.PCT) or (x2.value == 1 and x2.units == Unit.NONE): + node.removeAttribute('x2') + num += 1 - # y2: 0% - if node.getAttribute('y2') != '': - y2 = SVGLength(node.getAttribute('y2')) - if y2.value == 0: - node.removeAttribute('y2') - num += 1 + # y2: 0% + if node.getAttribute('y2') != '': + y2 = SVGLength(node.getAttribute('y2')) + if y2.value == 0: + node.removeAttribute('y2') + num += 1 - # fx: equal to rx - if node.getAttribute('fx') != '': - if node.getAttribute('fx') == node.getAttribute('cx'): - node.removeAttribute('fx') - num += 1 + # fx: equal to rx + if node.getAttribute('fx') != '': + if node.getAttribute('fx') == node.getAttribute('cx'): + node.removeAttribute('fx') + num += 1 - # fy: equal to ry - if node.getAttribute('fy') != '': - if node.getAttribute('fy') == node.getAttribute('cy'): - node.removeAttribute('fy') - num += 1 + # fy: equal to ry + if node.getAttribute('fy') != '': + if node.getAttribute('fy') == node.getAttribute('cy'): + node.removeAttribute('fy') + num += 1 - # cx: 50% - if node.getAttribute('cx') != '': - cx = SVGLength(node.getAttribute('cx')) - if (cx.value == 50 and cx.units == Unit.PCT) or (cx.value == 0.5 and cx.units == Unit.NONE): - node.removeAttribute('cx') - num += 1 + # cx: 50% + if node.getAttribute('cx') != '': + cx = SVGLength(node.getAttribute('cx')) + if (cx.value == 50 and cx.units == Unit.PCT) or (cx.value == 0.5 and cx.units == Unit.NONE): + node.removeAttribute('cx') + num += 1 - # cy: 50% - if node.getAttribute('cy') != '': - cy = SVGLength(node.getAttribute('cy')) - if (cy.value == 50 and cy.units == Unit.PCT) or (cy.value == 0.5 and cy.units == Unit.NONE): - node.removeAttribute('cy') - num += 1 + # cy: 50% + if node.getAttribute('cy') != '': + cy = SVGLength(node.getAttribute('cy')) + if (cy.value == 50 and cy.units == Unit.PCT) or (cy.value == 0.5 and cy.units == Unit.NONE): + node.removeAttribute('cy') + num += 1 - # r: 50% - if node.getAttribute('r') != '': - r = SVGLength(node.getAttribute('r')) - if (r.value == 50 and r.units == Unit.PCT) or (r.value == 0.5 and r.units == Unit.NONE): - node.removeAttribute('r') - num += 1 + # r: 50% + if node.getAttribute('r') != '': + r = SVGLength(node.getAttribute('r')) + if (r.value == 50 and r.units == Unit.PCT) or (r.value == 0.5 and r.units == Unit.NONE): + node.removeAttribute('r') + num += 1 - # Summarily get rid of some more attributes - attributes = [node.attributes.item(i).nodeName - for i in range(node.attributes.length)] - for attribute in attributes: - if attribute not in tainted: - if attribute in default_attributes.keys(): - if node.getAttribute(attribute) == default_attributes[attribute]: - node.removeAttribute(attribute) - num += 1 - else: - tainted = taint(tainted, attribute) - # These attributes might also occur as styles - styles = _getStyle(node) - for attribute in styles.keys(): - if attribute not in tainted: - if attribute in default_attributes.keys(): - if styles[attribute] == default_attributes[attribute]: - del styles[attribute] - num += 1 - else: - tainted = taint(tainted, attribute) - _setStyle(node, styles) + # Summarily get rid of some more attributes + attributes = [node.attributes.item(i).nodeName + for i in range(node.attributes.length)] + for attribute in attributes: + if attribute not in tainted: + if attribute in default_attributes.keys(): + if node.getAttribute(attribute) == default_attributes[attribute]: + node.removeAttribute(attribute) + num += 1 + else: + tainted = taint(tainted, attribute) + # These attributes might also occur as styles + styles = _getStyle(node) + for attribute in styles.keys(): + if attribute not in tainted: + if attribute in default_attributes.keys(): + if styles[attribute] == default_attributes[attribute]: + del styles[attribute] + num += 1 + else: + tainted = taint(tainted, attribute) + _setStyle(node, styles) - # recurse for our child elements - for child in node.childNodes : - num += removeDefaultAttributeValues(child, options, 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 colors.keys(): - s = colors[s] + if s in 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 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 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 xrange(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 xrange(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 xrange(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 xrange(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 + # 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 xrange(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 xrange(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 xrange(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 xrange(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 xrange(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 xrange(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 xrange(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 xrange(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 xrange(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) + x, y = startx, starty + i = 2 + for i in xrange(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 xrange(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 xrange(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 xrange(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 xrange(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 xrange(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 + # 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 xrange(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'] + # 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): - # 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] + # 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] - foundStraightCurve = False + foundStraightCurve = False - 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 + 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 - 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 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]) - i += 6 - if newData or cmd == 'z' or cmd == 'Z': - newPath.append( (cmd,newData) ) - path = newPath + 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 = [] + # 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) + # 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 - # 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': - bez_ctl_pt = (0,0) - 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 + # 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': + bez_ctl_pt = (0,0) + 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 + # 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 + 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 + 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 + 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 xrange(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 + # 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 xrange(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 = [] + # 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) + # 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 + # 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) - numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) - element.setAttribute('d', newPathStr) + newPathStr = serializePath(path, options) + 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 of containing an even number of coordinate strings - """ - i = 0 + Returns a list of 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.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.strip()) + nums = [] - # also, if 100-100 is found, split it into two also - # - for i in xrange(len(ws_nums)): - negcoords = ws_nums[i].split("-") + # also, if 100-100 is found, split it into two also + # + for i in xrange(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 xrange(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 = nums[len(nums)-1] - if 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 xrange(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 = nums[len(nums)-1] + if 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 decimal.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 decimal.InvalidOperation: # one of the lengths had a unit or is an invalid number + return [] - i += 2 + i += 2 - return nums + return nums def cleanPolygon(elem, options): - """ - Remove unnecessary closing point of polygon points attribute - """ - global numPointsRemovedFromPolygon + """ + 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)) + 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( ' ' ) + """ + 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 xrange(1, len(newData)): - if newData[i][0] == '-' and 'e' in newData[i - 1]: - newData[i - 1] += ' ' - return ''.join(newData) - else: - return ''.join(newData) + # 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 xrange(1, len(newData)): + if newData[i][0] == '-' and 'e' in newData[i - 1]: + newData[i - 1] += ' ' + 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. + """ + Scours the numeric part of a length only. Does not accept units. - This is faster than scourLength on elements guaranteed not to - contain units. - """ - # reduce to the proper number of digits - if not isinstance(length, Decimal): - length = getcontext().create_decimal(str(length)) - # if the value is an integer, it may still have .0[...] attached to it for some reason - # remove those - if int(length) == length: - length = getcontext().create_decimal(int(length)) + This is faster than scourLength on elements guaranteed not to + contain units. + """ + # reduce to the proper number of digits + if not isinstance(length, Decimal): + length = getcontext().create_decimal(str(length)) + # if the value is an integer, it may still have .0[...] attached to it for some reason + # remove those + if int(length) == length: + length = getcontext().create_decimal(int(length)) - # 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 = unicode(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 = unicode(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 - if len(nonsci) > 3: # avoid calling normalize unless strictly necessary - # and then the scientific notation version, with E+NUMBER replaced with - # just eNUMBER, since SVG accepts this. - sci = unicode(length.normalize()).lower().replace("e+", "e") + if len(nonsci) > 3: # avoid calling normalize unless strictly necessary + # and then the scientific notation version, with E+NUMBER replaced with + # just eNUMBER, since SVG accepts this. + sci = unicode(length.normalize()).lower().replace("e+", "e") - if len(sci) < len(nonsci): return sci - else: return nonsci - else: return nonsci + 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 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 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) + for child in element.childNodes: + if child.nodeType == 1: + num += reducePrecision(child) - return num + 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 - 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 - del transform[i] - else: - 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] + 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] + 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 + # 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) + for child in element.childNodes: + if child.nodeType == 1: + num += optimizeTransforms(child, options) - return num + return num def removeComments(element) : - """ - Removes comments from the element and its children. - """ - global numCommentBytes + """ + Removes comments from the element and its children. + """ + global numCommentBytes - if isinstance(element, xml.dom.minidom.Document): - # must process the document object separately, because its - # documentElement's nodes have None as their parentNode - for subelement in element.childNodes: - if isinstance(element, xml.dom.minidom.Comment): - numCommentBytes += len(element.data) - element.documentElement.removeChild(subelement) - else: - removeComments(subelement) - elif isinstance(element, xml.dom.minidom.Comment): - numCommentBytes += len(element.data) - element.parentNode.removeChild(element) - else: - for subelement in element.childNodes: - removeComments(subelement) + if isinstance(element, xml.dom.minidom.Document): + # must process the document object separately, because its + # documentElement's nodes have None as their parentNode + for subelement in element.childNodes: + if isinstance(element, xml.dom.minidom.Comment): + numCommentBytes += len(element.data) + element.documentElement.removeChild(subelement) + else: + removeComments(subelement) + elif 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 - """ - Converts raster references to inline images. - NOTE: there are size limits to base64-encoding handling in browsers - """ - global numRastersEmbedded + import base64 + import urllib + """ + Converts raster references to inline images. + NOTE: there are size limits to base64-encoding handling in browsers + """ + 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' + # 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 + 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 + # 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 + # 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: - return + # 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 + # 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') + # 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 xrange(attrList.length): - attr = attrList.item(i) - newNode.setAttributeNS( attr.namespaceURI, attr.localName, attr.nodeValue) + # add all the attributes + attrList = node.attributes + for i in xrange(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 + # 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) + # now do all child nodes + for child in node.childNodes : + remapNamespacePrefix(child, oldprefix, newprefix) def makeWellFormed(str): - xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} + xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} -# starr = [] -# for c in str: -# if c in xml_ents: -# starr.append(xml_ents[c]) -# else: -# starr.append(c) +# starr = [] +# for c in str: +# 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]) + # 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 = [] + outParts = [] - indent = ind - I='' - if options.indent_type == 'tab': I='\t' - elif options.indent_type == 'space': I=' ' + indent = ind + I='' + if options.indent_type == 'tab': I='\t' + elif options.indent_type == 'space': I=' ' - 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 - attrList = element.attributes - for num in xrange(attrList.length) : - attr = attrList.item(num) - 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 + attrList = element.attributes + for num in xrange(attrList.length) : + attr = attrList.item(num) + 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 ) + attrValue = makeWellFormed( attr.nodeValue ) - 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(['\n', 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(['\n', 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('\n') - else: - outParts.append('/>') - if indent > 0: outParts.append('\n') + if onNewLine: outParts.append(I * ind) + outParts.extend(['']) + if indent > 0: outParts.append('\n') + else: + outParts.append('/>') + if indent > 0: outParts.append('\n') - return "".join(outParts) + 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): - if options is None: - options = _options_parser.get_default_values() - getcontext().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) + if options is None: + options = _options_parser.get_default_values() + getcontext().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) - # 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 num in xrange(attrList.length) : - if attrList.item(num).nodeValue in unwanted_ns : - xmlnsDeclsToRemove.append(attrList.item(num).nodeName) + # remove the xmlns: declarations now + xmlnsDeclsToRemove = [] + attrList = doc.documentElement.attributes + for num in xrange(attrList.length) : + if attrList.item(num).nodeValue in unwanted_ns : + xmlnsDeclsToRemove.append(attrList.item(num).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 SVG namespace declaration - attrList = doc.documentElement.attributes - xmlnsDeclsToRemove = [] - redundantPrefixes = [] - for i in xrange(attrList.length): - attr = attrList.item(i) - name = attr.nodeName - val = attr.nodeValue - if name[0:6] == 'xmlns:' and val == 'http://www.w3.org/2000/svg': - redundantPrefixes.append(name[6:]) - xmlnsDeclsToRemove.append(name) + # check for redundant SVG namespace declaration + attrList = doc.documentElement.attributes + xmlnsDeclsToRemove = [] + redundantPrefixes = [] + for i in xrange(attrList.length): + attr = attrList.item(i) + name = attr.nodeName + val = attr.nodeValue + if name[0:6] == 'xmlns:' and val == 'http://www.w3.org/2000/svg': + redundantPrefixes.append(name[6:]) + xmlnsDeclsToRemove.append(name) - for attrName in xmlnsDeclsToRemove: - doc.documentElement.removeAttribute(attrName) + for attrName in xmlnsDeclsToRemove: + doc.documentElement.removeAttribute(attrName) - 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) - # 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 if the user wants to - if options.remove_metadata: - removeMetadataElements(doc) + # remove if the user wants to + if options.remove_metadata: + removeMetadataElements(doc) - # remove unreferenced gradients/patterns outside of defs - # and most unreferenced elements inside of defs - while removeUnreferencedElements(doc) > 0: - pass + # remove unreferenced gradients/patterns outside of defs + # and most unreferenced elements inside of defs + while removeUnreferencedElements(doc) > 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', '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 + # remove empty defs, metadata, g + # NOTE: these elements will be removed if they just have whitespace-only text nodes + for tag in ['defs', '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) + 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 + while removeDuplicateGradientStops(doc) > 0: + pass - # remove gradients that are only referenced by one other gradient - while collapseSinglyReferencedGradients(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 + # 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) + # 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) + # 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) + # 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 + # 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) + # 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) + # 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) + # 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, unprotected_ids(doc, options)) + # shorten ID names as much as possible + if options.shorten_ids: + numBytesSavedInIDs += shortenIDs(doc, 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))) + # 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) + # more length scouring in this function + numBytesSavedInLengths = reducePrecision(doc.documentElement) - # remove default values of attributes - numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) + # remove default values of attributes + numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) - # reduce the length of transformation attributes - numBytesSavedInTransforms = optimizeTransforms(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) + # 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) + # 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 -# out_string = doc.documentElement.toprettyxml(' ') - out_string = serializeXML(doc.documentElement, options) + '\n' + # 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' - # 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' + 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 + 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): - in_string = open(filename).read() - out_string = scourString(in_string, options) - return xml.dom.minidom.parseString(out_string.encode('utf-8')) + in_string = open(filename).read() + out_string = scourString(in_string, options) + return xml.dom.minidom.parseString(out_string.encode('utf-8')) # 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 [-i input.svg] [-o output.svg] [OPTIONS]", - description=("If the input/output files are specified with a svgz" - " extension, then compressed SVG is assumed. If the input file is not" - " specified, stdin is used. If the output file is not specified, " - " stdout is used."), - formatter=HeaderedFormatter(max_help_position=30), - version=VER) + usage="%prog [-i input.svg] [-o output.svg] [OPTIONS]", + description=("If the input/output files are specified with a svgz" + " extension, then compressed SVG is assumed. If the input file is not" + " specified, stdin is used. If the output file is not specified, " + " stdout is used."), + formatter=HeaderedFormatter(max_help_position=30), + version=VER) _options_parser.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") _options_parser.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") _options_parser.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") _options_parser.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") _options_parser.add_option("--enable-id-stripping", - action="store_true", dest="strip_ids", default=False, - help="remove all un-referenced ID attributes") + action="store_true", dest="strip_ids", default=False, + help="remove all un-referenced ID attributes") _options_parser.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") _options_parser.add_option("--shorten-ids", - action="store_true", dest="shorten_ids", default=False, - help="shorten all ID attributes to the least number of letters possible") + action="store_true", dest="shorten_ids", default=False, + help="shorten all ID attributes to the least number of letters possible") _options_parser.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") _options_parser.add_option("--keep-editor-data", - action="store_true", dest="keep_editor_data", default=False, - help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") + action="store_true", dest="keep_editor_data", default=False, + help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") _options_parser.add_option("--remove-metadata", - action="store_true", dest="remove_metadata", default=False, - help="remove elements (which may contain license metadata etc.)") + action="store_true", dest="remove_metadata", default=False, + help="remove elements (which may contain license metadata etc.)") _options_parser.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)") _options_parser.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("--strip-xml-prolog", - action="store_true", dest="strip_xml_prolog", default=False, - help="won't output the prolog") + action="store_true", dest="strip_xml_prolog", default=False, + help="won't output the prolog") _options_parser.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") # GZ: this is confusing, most people will be thinking in terms of # decimal places, which is not what decimal precision is doing _options_parser.add_option("-p", "--set-precision", - action="store", type=int, dest="digits", default=5, - help="set number of significant digits (default: %default)") + action="store", type=int, dest="digits", default=5, + help="set number of significant digits (default: %default)") _options_parser.add_option("-i", - action="store", dest="infilename", help=optparse.SUPPRESS_HELP) + action="store", dest="infilename", help=optparse.SUPPRESS_HELP) _options_parser.add_option("-o", - action="store", dest="outfilename", help=optparse.SUPPRESS_HELP) + action="store", dest="outfilename", help=optparse.SUPPRESS_HELP) _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("--indent", - action="store", type="string", dest="indent_type", default="space", - help="indentation of the output: none, space, tab (default: %default)") + action="store", type="string", dest="indent_type", default="space", + help="indentation of the output: none, space, tab (default: %default)") _options_parser.add_option("--protect-ids-noninkscape", - action="store_true", dest="protect_ids_noninkscape", default=False, - help="Don't change IDs not ending with a digit") + action="store_true", dest="protect_ids_noninkscape", default=False, + help="Don't change IDs not ending with a digit") _options_parser.add_option("--protect-ids-list", - action="store", type="string", dest="protect_ids_list", default=None, - help="Don't change IDs given in a comma-separated list") + action="store", type="string", dest="protect_ids_list", default=None, + help="Don't change IDs given in a comma-separated list") _options_parser.add_option("--protect-ids-prefix", - action="store", type="string", dest="protect_ids_prefix", default=None, - help="Don't change IDs starting with the given prefix") + action="store", type="string", dest="protect_ids_prefix", default=None, + help="Don't change IDs starting with the given prefix") 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 file(filename, mode) + if os.path.splitext(filename)[1].lower() in (".svgz", ".gz"): + import gzip + return gzip.GzipFile(filename, mode) + return file(filename, mode) def parse_args(args=None): - options, rargs = _options_parser.parse_args(args) + options, rargs = _options_parser.parse_args(args) - if 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.infilename and options.outfilename and options.infilename == options.outfilename: - _options_parser.error("Input filename is the same as output filename") + if 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.infilename and options.outfilename and options.infilename == options.outfilename: + _options_parser.error("Input filename is the same as output filename") - if options.infilename: - infile = maybe_gziped_file(options.infilename) - # GZ: could catch a raised IOError here and report - else: - # GZ: could sniff for gzip compression here - infile = sys.stdin - if options.outfilename: - outfile = maybe_gziped_file(options.outfilename, "wb") - else: - outfile = sys.stdout + if options.infilename: + infile = maybe_gziped_file(options.infilename) + # GZ: could catch a raised IOError here and report + else: + # GZ: could sniff for gzip compression here + infile = sys.stdin + if options.outfilename: + outfile = maybe_gziped_file(options.outfilename, "wb") + else: + outfile = sys.stdout - return options, [infile, outfile] + return options, [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 run(): - if sys.platform == "win32": - from time import clock as get_tick - else: - # GZ: is this different from time.time() in any way? - def get_tick(): - return os.times()[0] + if sys.platform == "win32": + from time import clock as get_tick + else: + # GZ: is this different from time.time() in any way? + def get_tick(): + return os.times()[0] - start = get_tick() + start = get_tick() - options, (input, output) = parse_args() + options, (input, output) = parse_args() - if not options.quiet: - print >>sys.stderr, "%s %s\n%s" % (APP, VER, COPYRIGHT) + if not options.quiet: + print >>sys.stderr, "%s %s\n%s" % (APP, VER, COPYRIGHT) - # 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 = get_tick() + end = get_tick() - # GZ: not using globals would be good too - if not options.quiet: - print >>sys.stderr, ' File:', input.name, \ - os.linesep + ' Time taken:', str(end-start) + 's' + os.linesep, \ - getReport() + # GZ: not using globals would be good too + if not options.quiet: + print >>sys.stderr, ' File:', input.name, \ + os.linesep + ' Time taken:', str(end-start) + 's' + os.linesep, \ + getReport() + + oldsize = len(in_string) + newsize = len(out_string) + sizediff = (newsize / oldsize) * 100 + print >>sys.stderr, ' Original file size:', oldsize, 'bytes;', \ + 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)' - oldsize = len(in_string) - newsize = len(out_string) - sizediff = (newsize / oldsize) * 100 - print >>sys.stderr, ' Original file size:', oldsize, 'bytes;', \ - 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)' if __name__ == '__main__': run() diff --git a/scour/svg_regex.py b/scour/svg_regex.py index 00b6158..ce83c7b 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -1,6 +1,6 @@ # This software is OSI Certified Open Source Software. # OSI Certified is a certification mark of the Open Source Initiative. -# +# # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # diff --git a/scour/svg_transform.py b/scour/svg_transform.py index 07b523c..72fd06f 100644 --- a/scour/svg_transform.py +++ b/scour/svg_transform.py @@ -149,8 +149,8 @@ class SVGTransformationParser(object): commands = [] token = next() while token[0] is not EOF: - command, token = self.rule_svg_transform(next, token) - commands.append(command) + command, token = self.rule_svg_transform(next, token) + commands.append(command) return commands def rule_svg_transform(self, next, token): @@ -177,7 +177,7 @@ class SVGTransformationParser(object): number, token = self.rule_optional_number(next, token) if number is not None: numbers.append(number) - + return numbers, token def rule_1number(self, next, token): @@ -199,10 +199,10 @@ class SVGTransformationParser(object): # but, if the 2nd number is provided, the 3rd is mandatory. # we can't have just 2. numbers.append(number) - + number, token = self.rule_number(next, token) numbers.append(number) - + return numbers, token def rule_6numbers(self, next, token): diff --git a/scour/yocto_css.py b/scour/yocto_css.py index a73e5f2..3efeeda 100644 --- a/scour/yocto_css.py +++ b/scour/yocto_css.py @@ -23,8 +23,8 @@ # scour needed a bare-minimum CSS parser in order to determine if some elements # were still referenced by CSS properties. -# I looked at css-py (a CSS parser built in Python), but that library -# is about 35k of Python and requires ply to be installed. I just need +# I looked at css-py (a CSS parser built in Python), but that library +# is about 35k of Python and requires ply to be installed. I just need # something very basic to suit scour's needs. # yocto-css takes a string of CSS and tries to spit out a list of rules @@ -45,28 +45,28 @@ # value : [ any | block | ATKEYWORD S* ]+; # any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING # | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES -# | DASHMATCH | FUNCTION S* any* ')' +# | 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 b09c304..30b9080 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ from setuptools import setup, find_packages setup ( name = 'scour', - version = '0.26', + version = '0.27', description = 'Scour SVG Optimizer', long_description = open("README.md").read(), license = 'Apache License 2.0', From 33535008453c066ccfa63854091549f7ef2f47ea Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 26 Oct 2013 17:47:42 +0200 Subject: [PATCH 002/270] allow direct calling of module --- scour/scour.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++---- setup.py | 17 +++++++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 7e538b1..34c6b05 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -70,7 +70,7 @@ except ImportError: pass APP = 'scour' -VER = '0.26' +VER = '0.27' COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2010' NS = { 'SVG': 'http://www.w3.org/2000/svg', @@ -2099,6 +2099,8 @@ def cleanPath(element, options) : numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) element.setAttribute('d', newPathStr) + + def parseListOfPoints(s): """ Parse string into a list of points. @@ -2155,6 +2157,8 @@ def parseListOfPoints(s): return nums + + def cleanPolygon(elem, options): """ Remove unnecessary closing point of polygon points attribute @@ -2171,6 +2175,8 @@ def cleanPolygon(elem, options): numPointsRemovedFromPolygon += 1 elem.setAttribute('points', scourCoordinates(pts, options, True)) + + def cleanPolyline(elem, options): """ Scour the polyline points attribute @@ -2178,6 +2184,8 @@ def cleanPolyline(elem, options): pts = parseListOfPoints(elem.getAttribute('points')) elem.setAttribute('points', scourCoordinates(pts, options, True)) + + def serializePath(pathObj, options): """ Reserializes the path data with some cleanups. @@ -2186,6 +2194,8 @@ def serializePath(pathObj, options): # 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. @@ -2197,6 +2207,8 @@ def serializeTransform(transformObj): for command, numbers in transformObj] ) + + def scourCoordinates(data, options, forceCommaWsp = False): """ Serializes coordinate data with some cleanups: @@ -2246,6 +2258,8 @@ def scourCoordinates(data, options, forceCommaWsp = False): return '' + + def scourLength(length): """ Scours a length. Accepts units. @@ -2254,6 +2268,8 @@ def scourLength(length): 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. @@ -2288,6 +2304,8 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a else: return nonsci else: return nonsci + + def reducePrecision(element) : """ Because opacities, letter spacings, stroke widths and all that don't need @@ -2333,6 +2351,8 @@ def reducePrecision(element) : return num + + def optimizeAngle(angle): """ Because any rotation can be expressed within 360 degrees @@ -2355,6 +2375,7 @@ def optimizeAngle(angle): return angle + def optimizeTransform(transform): """ Optimises a series of transformations parsed from a single @@ -2514,6 +2535,8 @@ def optimizeTransform(transform): else: i += 1 + + def optimizeTransforms(element, options) : """ Attempts to optimise transform specifications on the given node and its children. @@ -2544,6 +2567,8 @@ def optimizeTransforms(element, options) : return num + + def removeComments(element) : """ Removes comments from the element and its children. @@ -2566,6 +2591,8 @@ def removeComments(element) : for subelement in element.childNodes: removeComments(subelement) + + def embedRasters(element, options) : import base64 import urllib @@ -2623,6 +2650,8 @@ def embedRasters(element, options) : numRastersEmbedded += 1 del b64eRaster + + def properlySizeDoc(docElement, options): # get doc width and height w = SVGLength(docElement.getAttribute('width')) @@ -2663,6 +2692,8 @@ def properlySizeDoc(docElement, options): docElement.removeAttribute('width') docElement.removeAttribute('height') + + def remapNamespacePrefix(node, oldprefix, newprefix): if node == None or node.nodeType != 1: return @@ -2698,6 +2729,8 @@ def remapNamespacePrefix(node, oldprefix, newprefix): for child in node.childNodes : remapNamespacePrefix(child, oldprefix, newprefix) + + def makeWellFormed(str): xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} @@ -2711,6 +2744,8 @@ def makeWellFormed(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 @@ -2809,6 +2844,8 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): 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 @@ -3020,6 +3057,8 @@ def scourString(in_string, options=None): return total_output + + # used mostly by unit tests # input is a filename # returns the minidom doc representation of the SVG @@ -3028,6 +3067,8 @@ def scourXmlFile(filename, options=None): out_string = scourString(in_string, options) return xml.dom.minidom.parseString(out_string.encode('utf-8')) + + # GZ: Seems most other commandline tools don't do this, is it really wanted? class HeaderedFormatter(optparse.IndentedHelpFormatter): """ @@ -3038,6 +3079,8 @@ class HeaderedFormatter(optparse.IndentedHelpFormatter): 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( @@ -3117,12 +3160,16 @@ _options_parser.add_option("--protect-ids-prefix", action="store", type="string", dest="protect_ids_prefix", default=None, help="Don't change IDs starting with the given prefix") + + 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 file(filename, mode) + + def parse_args(args=None): options, rargs = _options_parser.parse_args(args) @@ -3148,6 +3195,8 @@ def parse_args(args=None): return options, [infile, outfile] + + def getReport(): return ' Number of elements removed: ' + str(numElemsRemoved) + os.linesep + \ ' Number of attributes removed: ' + str(numAttrsRemoved) + os.linesep + \ @@ -3164,7 +3213,20 @@ def getReport(): ' Number of bytes saved in transformations: ' + str(numBytesSavedInTransforms) -def run(): + +def generateDefaultOptions(): + ## FIXME: clean up this mess/hack and refactor arg parsing to argparse + class Struct: + def __init__(self, **entries): + self.__dict__.update(entries) + + d = parse_args()[0].__dict__.copy() + + return Struct(**d) + + + +def start(options, input, output): if sys.platform == "win32": from time import clock as get_tick else: @@ -3174,8 +3236,6 @@ def run(): start = get_tick() - options, (input, output) = parse_args() - if not options.quiet: print >>sys.stderr, "%s %s\n%s" % (APP, VER, COPYRIGHT) @@ -3203,6 +3263,12 @@ def run(): 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)' + +def run(): + options, (input, output) = parse_args() + start(options, input, output) + + + if __name__ == '__main__': run() - diff --git a/setup.py b/setup.py index 30b9080..73a7134 100644 --- a/setup.py +++ b/setup.py @@ -18,15 +18,28 @@ from setuptools import setup, find_packages +LONGDESC = """ +Scour is a SVG optimizer/sanitizer that can be used to produce SVGs for Web deployment. + +Website + - http://www.codedread.com/scour/ (original website) + - https://github.com/oberstet/scour (today) + +Authors: + - Jeff Schiller, Louis Simard (original authors) + - Tobias Oberstein (maintainer) +""" + setup ( name = 'scour', version = '0.27', description = 'Scour SVG Optimizer', - long_description = open("README.md").read(), +# long_description = open("README.md").read(), + long_description = LONGDESC, license = 'Apache License 2.0', author = 'Jeff Schiller', author_email = 'codedread@gmail.com', - url = 'http://blog.codedread.com/', + url = 'https://github.com/oberstet/scour', platforms = ('Any'), install_requires = [], packages = find_packages(), From ae4d9303f1ff8748cd3e0d62046d6ace66fb39a9 Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Fri, 10 Jan 2014 23:42:02 +0100 Subject: [PATCH 003/270] support custom id prefixes --- scour/scour.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 34c6b05..900070c 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -620,7 +620,7 @@ def removeUnreferencedElements(doc): num += 1 return num -def shortenIDs(doc, unprotectedElements=None): +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. @@ -647,21 +647,21 @@ def shortenIDs(doc, unprotectedElements=None): curIdNum = 1 for rid in idList: - curId = intToID(curIdNum) + 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) + curId = intToID(curIdNum, prefix) # Then go rename it. num += renameID(doc, rid, curId, identifiedElements, referencedIDs) curIdNum += 1 return num -def intToID(idnum): +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. @@ -673,7 +673,7 @@ def intToID(idnum): rid = chr((idnum % 26) + ord('a')) + rid idnum = int(idnum / 26) - return rid + return prefix + rid def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): """ @@ -3001,7 +3001,7 @@ def scourString(in_string, options=None): # shorten ID names as much as possible if options.shorten_ids: - numBytesSavedInIDs += shortenIDs(doc, unprotected_ids(doc, options)) + 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']: @@ -3113,6 +3113,9 @@ _options_parser.add_option("--enable-comment-stripping", _options_parser.add_option("--shorten-ids", action="store_true", dest="shorten_ids", default=False, help="shorten all ID attributes to the least number of letters possible") +_options_parser.add_option("--shorten-ids-prefix", + action="store", type="string", dest="shorten_ids_prefix", default="", + help="shorten all ID attributes with a custom prefix") _options_parser.add_option("--disable-embed-rasters", action="store_false", dest="embed_rasters", default=True, help="won't embed rasters as base64-encoded data") From edcaeba905d9da6b22a0a45a9fef3e9020b8519a Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Sun, 12 Jan 2014 16:56:56 +0100 Subject: [PATCH 004/270] fixed version string --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 900070c..3210b1a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -70,7 +70,7 @@ except ImportError: pass APP = 'scour' -VER = '0.27' +VER = '0.28' COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2010' NS = { 'SVG': 'http://www.w3.org/2000/svg', From 9bb929d91e07a6265cc89ec2b5e4c613f61c989c Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Sun, 12 Jan 2014 17:10:02 +0100 Subject: [PATCH 005/270] added option to keep elements within defs --- scour/scour.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 900070c..336ca86 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -587,7 +587,7 @@ def removeUnusedDefs(doc, defElem, elemsToRemove=None): elemsToRemove.append(elem) return elemsToRemove -def removeUnreferencedElements(doc): +def removeUnreferencedElements(doc, keepDefs): """ Removes all unreferenced elements except for , , , , and . Also vacuums the defs of any non-referenced renderable elements. @@ -610,14 +610,15 @@ def removeUnreferencedElements(doc): num += 1 numElemsRemoved += 1 - # 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 + 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): @@ -2923,7 +2924,7 @@ def scourString(in_string, options=None): # remove unreferenced gradients/patterns outside of defs # and most unreferenced elements inside of defs - while removeUnreferencedElements(doc) > 0: + while removeUnreferencedElements(doc, options.keep_defs) > 0: pass # remove empty defs, metadata, g @@ -3119,6 +3120,9 @@ _options_parser.add_option("--shorten-ids-prefix", _options_parser.add_option("--disable-embed-rasters", action="store_false", dest="embed_rasters", default=True, help="won't embed rasters as base64-encoded data") +_options_parser.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") _options_parser.add_option("--keep-editor-data", action="store_true", dest="keep_editor_data", default=False, help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") From f7165f66a92e44630dc9038e4752f335e0d5b0d5 Mon Sep 17 00:00:00 2001 From: mdxs Date: Sun, 9 Mar 2014 23:44:06 +0100 Subject: [PATCH 006/270] Small typo fix in doc string of parseListOfPoints --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 900070c..752c56b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2105,7 +2105,7 @@ def parseListOfPoints(s): """ Parse string into a list of points. - Returns a list of containing an even number of coordinate strings + Returns a list containing an even number of coordinate strings """ i = 0 From 26a360a00c143a60e3bd444c4a5217b54fbdda53 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 26 Jul 2014 17:20:47 +0200 Subject: [PATCH 007/270] add option to ingore unknown cmd line opts; bump version --- scour/scour.py | 9 +++++---- setup.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 0ef0507..f85a8ed 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -5,6 +5,7 @@ # # Copyright 2010 Jeff Schiller # Copyright 2010 Louis Simard +# Copyright 2013-2014 Tavendo GmbH # # This file is part of Scour, http://www.codedread.com/scour/ # @@ -70,7 +71,7 @@ except ImportError: pass APP = 'scour' -VER = '0.28' +VER = '0.29' COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2010' NS = { 'SVG': 'http://www.w3.org/2000/svg', @@ -3177,10 +3178,10 @@ def maybe_gziped_file(filename, mode="r"): -def parse_args(args=None): +def parse_args(args=None, ignore_additional_args=False): options, rargs = _options_parser.parse_args(args) - if rargs: + if rargs and not ignore_additional_args: _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") @@ -3227,7 +3228,7 @@ def generateDefaultOptions(): def __init__(self, **entries): self.__dict__.update(entries) - d = parse_args()[0].__dict__.copy() + d = parse_args(ignore_additional_args = True)[0].__dict__.copy() return Struct(**d) diff --git a/setup.py b/setup.py index 73a7134..97d11c2 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ ############################################################################### ## -## Copyright (C) 2013 Tavendo GmbH +## 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. @@ -32,7 +32,7 @@ Authors: setup ( name = 'scour', - version = '0.27', + version = '0.29', description = 'Scour SVG Optimizer', # long_description = open("README.md").read(), long_description = LONGDESC, From 85dff51cc94e73f99dae2759ea18e6672af48c17 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Tue, 5 Aug 2014 12:47:33 +0200 Subject: [PATCH 008/270] fix ingoring of additional args when invoked from scons; bump version --- scour/scour.py | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index f85a8ed..d79f0bf 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -71,10 +71,10 @@ except ImportError: pass APP = 'scour' -VER = '0.29' +VER = '0.30' COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2010' -NS = { 'SVG': 'http://www.w3.org/2000/svg', +NS = {'SVG': 'http://www.w3.org/2000/svg', 'XLINK': 'http://www.w3.org/1999/xlink', 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', 'INKSCAPE': 'http://www.inkscape.org/namespaces/inkscape', @@ -3228,7 +3228,7 @@ def generateDefaultOptions(): def __init__(self, **entries): self.__dict__.update(entries) - d = parse_args(ignore_additional_args = True)[0].__dict__.copy() + d = parse_args(args = [], ignore_additional_args = True)[0].__dict__.copy() return Struct(**d) diff --git a/setup.py b/setup.py index 97d11c2..4802ba1 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ Authors: setup ( name = 'scour', - version = '0.29', + version = '0.30', description = 'Scour SVG Optimizer', # long_description = open("README.md").read(), long_description = LONGDESC, From d1c66cc75bbe474b5a45a7cd45ed27207311d897 Mon Sep 17 00:00:00 2001 From: Andy Levisay Date: Tue, 28 Apr 2015 15:26:55 -0500 Subject: [PATCH 009/270] Python 3 Updates Ran python-modernizer, this fixed most of the Python 3 incompatibilities. Had to manually rework the iteration in svg_regex and svg_transform; .next doesn't exist in Python 3. Wrapped the builtin next function in a partial for equivalent functionality. --- scour/__init__.py | 5 -- scour/scour.py | 160 +++++++++++++++++++++-------------------- scour/svg_regex.py | 82 ++++++++++----------- scour/svg_transform.py | 59 ++++++++------- setup.py | 2 +- 5 files changed, 158 insertions(+), 150 deletions(-) diff --git a/scour/__init__.py b/scour/__init__.py index f3f6b3e..1282886 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -15,8 +15,3 @@ ## limitations under the License. ## ############################################################################### - -import scour -import svg_regex -import svg_transform -import yocto_css diff --git a/scour/scour.py b/scour/scour.py index d79f0bf..34e571d 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -47,21 +47,27 @@ # 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 + import os import sys import xml.dom.minidom import re import math -from svg_regex import svg_parser -from svg_transform import svg_transform_parser +from .svg_regex import svg_parser +from .svg_transform import svg_transform_parser import optparse -from yocto_css import parseCssString +from .yocto_css import parseCssString +import six +from six.moves import range # Python 2.3- did not have Decimal try: - from decimal import * + from decimal import Decimal, InvalidOperation, getcontext except ImportError: - print >>sys.stderr, "Scour requires Python 2.4." + print("Scour requires Python 2.4.", file=sys.stderr) # Import Psyco if available try: @@ -531,7 +537,7 @@ def findReferencingProperty(node, prop, val, ids): if prop in referencingProps and val != '' : if len(val) >= 7 and val[0:5] == 'url(#' : id = val[5:val.find(')')] - if ids.has_key(id) : + if id in ids : ids[id][0] += 1 ids[id][1].append(node) else: @@ -546,7 +552,7 @@ def findReferencingProperty(node, prop, val, ids): elif val[0:6] == "url('#" : id = val[6:val.find("')")] if id != None: - if ids.has_key(id) : + if id in ids : ids[id][0] += 1 ids[id][1].append(node) else: @@ -762,7 +768,7 @@ def unprotected_ids(doc, options): protect_ids_list = options.protect_ids_list.split(",") if options.protect_ids_prefix: protect_ids_prefixes = options.protect_ids_prefix.split(",") - for id in identifiedElements.keys(): + for id in list(identifiedElements.keys()): protected = False if options.protect_ids_noninkscape and not id[-1].isdigit(): protected = True @@ -785,9 +791,9 @@ def removeUnreferencedIDs(referencedIDs, identifiedElements): global numIDsRemoved keepTags = ['font'] num = 0; - for id in identifiedElements.keys(): + for id in list(identifiedElements.keys()): node = identifiedElements[id] - if referencedIDs.has_key(id) == False and not node.nodeName in keepTags: + if (id in referencedIDs) == False and not node.nodeName in keepTags: node.removeAttribute('id') numIDsRemoved += 1 num += 1 @@ -800,7 +806,7 @@ def removeNamespacedAttributes(node, namespaces): # remove all namespace'd attributes from this element attrList = node.attributes attrsToRemove = [] - for attrNum in xrange(attrList.length): + for attrNum in range(attrList.length): attr = attrList.item(attrNum) if attr != None and attr.namespaceURI in namespaces: attrsToRemove.append(attr.nodeName) @@ -915,7 +921,7 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): # 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 num in xrange(attrList.length): + for num in range(attrList.length): attr = attrList.item(num) # 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 @@ -934,7 +940,7 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): commonAttrs[attr.nodeName] = attr.nodeValue # for each subsequent child element - for childNum in xrange(len(childElements)): + for childNum in range(len(childElements)): # skip first child if childNum == 0: continue @@ -946,7 +952,7 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): distinctAttrs = [] # loop through all current 'common' attributes - for name in commonAttrs.keys(): + 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) @@ -955,7 +961,7 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): del commonAttrs[name] # commonAttrs now has all the inheritable attributes which are common among all child elements - for name in commonAttrs.keys(): + for name in list(commonAttrs.keys()): for child in childElements: child.removeAttribute(name) elem.setAttribute(name, commonAttrs[name]) @@ -1088,7 +1094,7 @@ def removeUnusedAttributesOnParent(elem): # get all attribute values on this parent attrList = elem.attributes unusedAttrs = {} - for num in xrange(attrList.length): + for num in range(attrList.length): attr = attrList.item(num) if attr.nodeName in ['clip-rule', 'display-align', @@ -1104,10 +1110,10 @@ def removeUnusedAttributesOnParent(elem): unusedAttrs[attr.nodeName] = attr.nodeValue # for each child, if at least one child inherits the parent's attribute, then remove - for childNum in xrange(len(childElements)): + for childNum in range(len(childElements)): child = childElements[childNum] inheritedAttrs = [] - for name in unusedAttrs.keys(): + for name in list(unusedAttrs.keys()): val = child.getAttribute(name) if val == '' or val == None or val == 'inherit': inheritedAttrs.append(name) @@ -1115,7 +1121,7 @@ def removeUnusedAttributesOnParent(elem): del unusedAttrs[a] # unusedAttrs now has all the parent attributes that are unused - for name in unusedAttrs.keys(): + for name in list(unusedAttrs.keys()): elem.removeAttribute(name) num += 1 @@ -1145,7 +1151,7 @@ def removeDuplicateGradientStops(doc): color = stop.getAttribute('stop-color') opacity = stop.getAttribute('stop-opacity') style = stop.getAttribute('style') - if stops.has_key(offset) : + if offset in stops : oldStop = stops[offset] if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: stopsToRemove.append(stop) @@ -1166,7 +1172,7 @@ def collapseSinglyReferencedGradients(doc): identifiedElements = findElementsWithId(doc.documentElement) # make sure to reset the ref'ed ids for when we are running this in testscour - for rid,nodeCount in findReferencedElements(doc.documentElement).iteritems(): + 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. @@ -1253,7 +1259,7 @@ def removeDuplicateGradients(doc): # now compare stops stopsNotEqual = False - for i in xrange(stops.length): + for i in range(stops.length): if stopsNotEqual: break stop = stops.item(i) ostop = ostops.item(i) @@ -1265,16 +1271,16 @@ def removeDuplicateGradients(doc): # ograd is a duplicate of grad, we schedule it to be removed UNLESS # ograd is ALREADY considered a 'master' element - if not gradientsToRemove.has_key(ograd): - if not duplicateToMaster.has_key(ograd): - if not gradientsToRemove.has_key(grad): + 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 gradientsToRemove.keys(): + for masterGrad in list(gradientsToRemove.keys()): master_id = masterGrad.getAttribute('id') # print 'master='+master_id for dupGrad in gradientsToRemove[masterGrad]: @@ -1322,7 +1328,7 @@ def _getStyle(node): def _setStyle(node, styleMap): u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" - fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap.keys()]) + fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in list(styleMap.keys())]) if fixedStyle != '' : node.setAttribute('style', fixedStyle) elif node.getAttribute('style'): @@ -1337,7 +1343,7 @@ def repairStyle(node, options): # 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 styleMap.has_key(prop) : + 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] + ')' @@ -1345,23 +1351,23 @@ def repairStyle(node, options): # Here is where we can weed out unnecessary styles like: # opacity:1 - if styleMap.has_key('opacity') : + 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 styleMap.has_key(uselessStyle): + if uselessStyle in styleMap: 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 styleMap.has_key('stroke') and styleMap['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 styleMap.has_key(strokestyle) : + if strokestyle in styleMap : del styleMap[strokestyle] num += 1 # TODO: This is actually a problem if a parent element has a specified stroke @@ -1369,38 +1375,38 @@ def repairStyle(node, options): del styleMap['stroke'] # if fill:none, then remove all fill-related properties (fill-rule, etc) - if styleMap.has_key('fill') and styleMap['fill'] == 'none' : + if 'fill' in styleMap and styleMap['fill'] == 'none' : for fillstyle in [ 'fill-rule', 'fill-opacity' ] : - if styleMap.has_key(fillstyle) : + if fillstyle in styleMap : del styleMap[fillstyle] num += 1 # fill-opacity: 0 - if styleMap.has_key('fill-opacity') : + if 'fill-opacity' in styleMap : fillOpacity = float(styleMap['fill-opacity']) if fillOpacity == 0.0 : for uselessFillStyle in [ 'fill', 'fill-rule' ] : - if styleMap.has_key(uselessFillStyle): + if uselessFillStyle in styleMap: del styleMap[uselessFillStyle] num += 1 # stroke-opacity: 0 - if styleMap.has_key('stroke-opacity') : + 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 styleMap.has_key(uselessStrokeStyle): + if uselessStrokeStyle in styleMap: del styleMap[uselessStrokeStyle] num += 1 # stroke-width: 0 - if styleMap.has_key('stroke-width') : + 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 styleMap.has_key(uselessStrokeStyle): + if uselessStrokeStyle in styleMap: del styleMap[uselessStrokeStyle] num += 1 @@ -1413,18 +1419,18 @@ def repairStyle(node, options): 'text-align', 'text-anchor', 'text-decoration', 'text-rendering', 'unicode-bidi', 'word-spacing', 'writing-mode'] : - if styleMap.has_key(fontstyle) : + 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 styleMap.has_key(inkscapeStyle): + if inkscapeStyle in styleMap: del styleMap[inkscapeStyle] num += 1 - if styleMap.has_key('overflow') : + if 'overflow' in styleMap : # overflow specified on element other than svg, marker, pattern if not node.nodeName in ['svg','marker','pattern']: del styleMap['overflow'] @@ -1445,7 +1451,7 @@ def repairStyle(node, options): # 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 styleMap.keys() : + for propName in list(styleMap.keys()) : if propName in svgAttributes : node.setAttribute(propName, styleMap[propName]) del styleMap[propName] @@ -1594,7 +1600,7 @@ def removeDefaultAttributeValues(node, options, tainted=set()): for i in range(node.attributes.length)] for attribute in attributes: if attribute not in tainted: - if attribute in default_attributes.keys(): + if attribute in list(default_attributes.keys()): if node.getAttribute(attribute) == default_attributes[attribute]: node.removeAttribute(attribute) num += 1 @@ -1602,9 +1608,9 @@ def removeDefaultAttributeValues(node, options, tainted=set()): tainted = taint(tainted, attribute) # These attributes might also occur as styles styles = _getStyle(node) - for attribute in styles.keys(): + for attribute in list(styles.keys()): if attribute not in tainted: - if attribute in default_attributes.keys(): + if attribute in list(default_attributes.keys()): if styles[attribute] == default_attributes[attribute]: del styles[attribute] num += 1 @@ -1626,7 +1632,7 @@ def convertColor(value): """ s = value - if s in colors.keys(): + if s in list(colors.keys()): s = colors[s] rgbpMatch = rgbp.match(s) @@ -1680,7 +1686,7 @@ def convertColors(element) : element.setAttribute(attr, newColorValue) numBytes += (oldBytes - len(element.getAttribute(attr))) # colors might also hide in styles - if attr in styles.keys(): + if attr in list(styles.keys()): oldColorValue = styles[attr] newColorValue = convertColor(oldColorValue) oldBytes = len(oldColorValue) @@ -1722,13 +1728,13 @@ 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. - for pathIndex in xrange(0, len(path)): + 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 xrange(i, len(data), 7): + for i in range(i, len(data), 7): data[i+5] -= x data[i+6] -= y x += data[i+5] @@ -1738,14 +1744,14 @@ def cleanPath(element, options) : x += sum(data[5::7]) y += sum(data[6::7]) elif cmd == 'H': - for i in xrange(i, len(data)): + 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 xrange(i, len(data)): + for i in range(i, len(data)): data[i] -= y y += data[i] path[pathIndex] = ('v', data) @@ -1761,14 +1767,14 @@ def cleanPath(element, options) : x, y = startx, starty i = 2 - for i in xrange(i, len(data), 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 xrange(i, len(data), 2): + for i in range(i, len(data), 2): data[i] -= x data[i+1] -= y x += data[i] @@ -1784,14 +1790,14 @@ def cleanPath(element, options) : else: startx = x + data[0] starty = y + data[1] - for i in xrange(i, len(data), 2): + 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 xrange(i, len(data), 4): + for i in range(i, len(data), 4): data[i] -= x data[i+1] -= y data[i+2] -= x @@ -1803,7 +1809,7 @@ def cleanPath(element, options) : x += sum(data[2::4]) y += sum(data[3::4]) elif cmd == 'C': - for i in xrange(i, len(data), 6): + for i in range(i, len(data), 6): data[i] -= x data[i+1] -= y data[i+2] -= x @@ -1824,7 +1830,7 @@ def cleanPath(element, options) : # 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 xrange(0, len(path)): + for pathIndex in range(0, len(path)): cmd, data = path[pathIndex] i = 0 if cmd in ['m','l','t']: @@ -2059,7 +2065,7 @@ def cleanPath(element, options) : # 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 xrange(1, len(path)): + for pathIndex in range(1, len(path)): cmd, data = path[pathIndex] if cmd in ['h','v'] and len(data) > 1: coordIndex = 1 @@ -2120,7 +2126,7 @@ def parseListOfPoints(s): # also, if 100-100 is found, split it into two also # - for i in xrange(len(ws_nums)): + for i in range(len(ws_nums)): negcoords = ws_nums[i].split("-") # this string didn't have any negative coordinates @@ -2128,7 +2134,7 @@ def parseListOfPoints(s): nums.append(negcoords[0]) # we got negative coords else: - for j in xrange(len(negcoords)): + for j in range(len(negcoords)): # first number could be positive if j == 0: if negcoords[0] != '': @@ -2152,7 +2158,7 @@ def parseListOfPoints(s): try: nums[i] = getcontext().create_decimal(nums[i]) nums[i + 1] = getcontext().create_decimal(nums[i + 1]) - except decimal.InvalidOperation: # one of the lengths had a unit or is an invalid number + except InvalidOperation: # one of the lengths had a unit or is an invalid number return [] i += 2 @@ -2251,7 +2257,7 @@ def scourCoordinates(data, options, forceCommaWsp = False): # separate from the next number. if options.renderer_workaround: if len(newData) > 0: - for i in xrange(1, len(newData)): + for i in range(1, len(newData)): if newData[i][0] == '-' and 'e' in newData[i - 1]: newData[i - 1] += ' ' return ''.join(newData) @@ -2290,7 +2296,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a # 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 = unicode(length).lower().replace("e+", "e") + 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 @@ -2300,7 +2306,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a if len(nonsci) > 3: # avoid calling normalize unless strictly necessary # and then the scientific notation version, with E+NUMBER replaced with # just eNUMBER, since SVG accepts this. - sci = unicode(length.normalize()).lower().replace("e+", "e") + sci = six.text_type(length.normalize()).lower().replace("e+", "e") if len(sci) < len(nonsci): return sci else: return nonsci @@ -2337,7 +2343,7 @@ def reducePrecision(element) : num += len(val) - len(newVal) element.setAttribute(lengthAttr, newVal) # repeat for attributes hidden in styles - if lengthAttr in styles.keys(): + if lengthAttr in list(styles.keys()): val = styles[lengthAttr] valLen = SVGLength(val) if valLen.units != Unit.INVALID: @@ -2714,7 +2720,7 @@ def remapNamespacePrefix(node, oldprefix, newprefix): # add all the attributes attrList = node.attributes - for i in xrange(attrList.length): + for i in range(attrList.length): attr = attrList.item(i) newNode.setAttributeNS( attr.namespaceURI, attr.localName, attr.nodeValue) @@ -2778,7 +2784,7 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): # now serialize the other attributes attrList = element.attributes - for num in xrange(attrList.length) : + for num in range(attrList.length) : attr = attrList.item(num) if attr.nodeName == 'id' or attr.nodeName == 'xml:id': continue # if the attribute value contains a double-quote, use single-quotes @@ -2877,7 +2883,7 @@ def scourString(in_string, options=None): # remove the xmlns: declarations now xmlnsDeclsToRemove = [] attrList = doc.documentElement.attributes - for num in xrange(attrList.length) : + for num in range(attrList.length) : if attrList.item(num).nodeValue in unwanted_ns : xmlnsDeclsToRemove.append(attrList.item(num).nodeName) @@ -2895,7 +2901,7 @@ def scourString(in_string, options=None): attrList = doc.documentElement.attributes xmlnsDeclsToRemove = [] redundantPrefixes = [] - for i in xrange(attrList.length): + for i in range(attrList.length): attr = attrList.item(i) name = attr.nodeName val = attr.nodeValue @@ -3174,7 +3180,7 @@ 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 file(filename, mode) + return open(filename, mode) @@ -3245,7 +3251,7 @@ def start(options, input, output): start = get_tick() if not options.quiet: - print >>sys.stderr, "%s %s\n%s" % (APP, VER, COPYRIGHT) + print("%s %s\n%s" % (APP, VER, COPYRIGHT), file=sys.stderr) # do the work in_string = input.read() @@ -3260,15 +3266,15 @@ def start(options, input, output): # GZ: not using globals would be good too if not options.quiet: - print >>sys.stderr, ' File:', input.name, \ + print(' File:', input.name, \ os.linesep + ' Time taken:', str(end-start) + 's' + os.linesep, \ - getReport() + getReport(), file=sys.stderr) oldsize = len(in_string) newsize = len(out_string) sizediff = (newsize / oldsize) * 100 - print >>sys.stderr, ' Original file size:', oldsize, 'bytes;', \ - 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)' + print(' Original file size:', oldsize, 'bytes;', \ + 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)', file=sys.stderr) diff --git a/scour/svg_regex.py b/scour/svg_regex.py index ce83c7b..e6659e5 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -41,10 +41,11 @@ Out[4]: [('M', [(0.60509999999999997, 0.5)])] In [5]: svg_parser.parse('M 100-200') # Another edge case Out[5]: [('M', [(100.0, -200.0)])] """ +from __future__ import absolute_import import re from decimal import * - +from functools import partial # Sentinel. class _EOF(object): @@ -145,140 +146,141 @@ class SVGPathParser(object): def parse(self, text): """ Parse a string of SVG data. """ - next = self.lexer.lex(text).next - token = next() - return self.rule_svg_path(next, token) + gen = self.lexer.lex(text) + next_val_fn = partial(next, *(gen,)) + token = next_val_fn() + return self.rule_svg_path(next_val_fn, token) - def rule_svg_path(self, next, token): + def rule_svg_path(self, next_val_fn, token): commands = [] while token[0] is not EOF: if token[0] != 'command': raise SyntaxError("expecting a command; got %r" % (token,)) rule = self.command_dispatch[token[1]] - command_group, token = rule(next, token) + command_group, token = rule(next_val_fn, token) commands.append(command_group) return commands - def rule_closepath(self, next, token): + def rule_closepath(self, next_val_fn, token): command = token[1] - token = next() + token = next_val_fn() return (command, []), token - def rule_moveto_or_lineto(self, next, token): + def rule_moveto_or_lineto(self, next_val_fn, token): command = token[1] - token = next() + token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: - pair, token = self.rule_coordinate_pair(next, token) + pair, token = self.rule_coordinate_pair(next_val_fn, token) coordinates.extend(pair) return (command, coordinates), token - def rule_orthogonal_lineto(self, next, token): + def rule_orthogonal_lineto(self, next_val_fn, token): command = token[1] - token = next() + token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: - coord, token = self.rule_coordinate(next, token) + coord, token = self.rule_coordinate(next_val_fn, token) coordinates.append(coord) return (command, coordinates), token - def rule_curveto3(self, next, token): + def rule_curveto3(self, next_val_fn, token): command = token[1] - token = next() + token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next, token) - pair2, token = self.rule_coordinate_pair(next, token) - pair3, token = self.rule_coordinate_pair(next, token) + pair1, token = self.rule_coordinate_pair(next_val_fn, token) + pair2, token = self.rule_coordinate_pair(next_val_fn, token) + pair3, token = self.rule_coordinate_pair(next_val_fn, token) coordinates.extend(pair1) coordinates.extend(pair2) coordinates.extend(pair3) return (command, coordinates), token - def rule_curveto2(self, next, token): + def rule_curveto2(self, next_val_fn, token): command = token[1] - token = next() + token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next, token) - pair2, token = self.rule_coordinate_pair(next, token) + pair1, token = self.rule_coordinate_pair(next_val_fn, token) + pair2, token = self.rule_coordinate_pair(next_val_fn, token) coordinates.extend(pair1) coordinates.extend(pair2) return (command, coordinates), token - def rule_curveto1(self, next, token): + def rule_curveto1(self, next_val_fn, token): command = token[1] - token = next() + token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next, token) + pair1, token = self.rule_coordinate_pair(next_val_fn, token) coordinates.extend(pair1) return (command, coordinates), token - def rule_elliptical_arc(self, next, token): + def rule_elliptical_arc(self, next_val_fn, token): command = token[1] - token = next() + token = next_val_fn() arguments = [] while token[0] in self.number_tokens: rx = Decimal(token[1]) * 1 if rx < Decimal("0.0"): raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) - token = next() + token = next_val_fn() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) ry = Decimal(token[1]) * 1 if ry < Decimal("0.0"): raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) - token = next() + token = next_val_fn() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) axis_rotation = Decimal(token[1]) * 1 - token = next() + token = next_val_fn() if token[1] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) large_arc_flag = Decimal(token[1]) * 1 - token = next() + token = next_val_fn() if token[1] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) sweep_flag = Decimal(token[1]) * 1 - token = next() + token = next_val_fn() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = Decimal(token[1]) * 1 - token = next() + token = next_val_fn() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) y = Decimal(token[1]) * 1 - token = next() + token = next_val_fn() arguments.extend([rx, ry, axis_rotation, large_arc_flag, sweep_flag, x, y]) return (command, arguments), token - def rule_coordinate(self, next, token): + def rule_coordinate(self, next_val_fn, token): if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = getcontext().create_decimal(token[1]) - token = next() + token = next_val_fn() return x, token - def rule_coordinate_pair(self, next, 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: raise SyntaxError("expecting a number; got %r" % (token,)) x = getcontext().create_decimal(token[1]) - token = next() + token = next_val_fn() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) y = getcontext().create_decimal(token[1]) - token = next() + token = next_val_fn() return [x, y], token diff --git a/scour/svg_transform.py b/scour/svg_transform.py index 72fd06f..85507ca 100644 --- a/scour/svg_transform.py +++ b/scour/svg_transform.py @@ -56,9 +56,12 @@ Multiple transformations are supported: In [12]: svg_transform_parser.parse('translate(30 -30) rotate(36)') Out[12]: [('translate', [30.0, -30.0]), ('rotate', [36.0])] """ +from __future__ import absolute_import import re from decimal import * +from six.moves import range +from functools import partial # Sentinel. @@ -145,88 +148,90 @@ class SVGTransformationParser(object): def parse(self, text): """ Parse a string of SVG transform="" data. """ - next = self.lexer.lex(text).next + gen = self.lexer.lex(text) + next_val_fn = partial(next, *(gen,)) + commands = [] - token = next() + token = next_val_fn() while token[0] is not EOF: - command, token = self.rule_svg_transform(next, token) + command, token = self.rule_svg_transform(next_val_fn, token) commands.append(command) return commands - def rule_svg_transform(self, next, token): + def rule_svg_transform(self, next_val_fn, token): if token[0] != 'command': raise SyntaxError("expecting a transformation type; got %r" % (token,)) command = token[1] rule = self.command_dispatch[command] - token = next() + token = next_val_fn() if token[0] != 'coordstart': raise SyntaxError("expecting '('; got %r" % (token,)) - numbers, token = rule(next, token) + numbers, token = rule(next_val_fn, token) if token[0] != 'coordend': raise SyntaxError("expecting ')'; got %r" % (token,)) - token = next() + token = next_val_fn() return (command, numbers), token - def rule_1or2numbers(self, next, token): + def rule_1or2numbers(self, next_val_fn, token): numbers = [] # 1st number is mandatory - token = next() - number, token = self.rule_number(next, token) + token = next_val_fn() + number, token = self.rule_number(next_val_fn, token) numbers.append(number) # 2nd number is optional - number, token = self.rule_optional_number(next, token) + number, token = self.rule_optional_number(next_val_fn, token) if number is not None: numbers.append(number) return numbers, token - def rule_1number(self, next, token): + def rule_1number(self, next_val_fn, token): # this number is mandatory - token = next() - number, token = self.rule_number(next, token) + token = next_val_fn() + number, token = self.rule_number(next_val_fn, token) numbers = [number] return numbers, token - def rule_1or3numbers(self, next, token): + def rule_1or3numbers(self, next_val_fn, token): numbers = [] # 1st number is mandatory - token = next() - number, token = self.rule_number(next, token) + token = next_val_fn() + number, token = self.rule_number(next_val_fn, token) numbers.append(number) # 2nd number is optional - number, token = self.rule_optional_number(next, token) + number, token = self.rule_optional_number(next_val_fn, token) if number is not None: # but, if the 2nd number is provided, the 3rd is mandatory. # we can't have just 2. numbers.append(number) - number, token = self.rule_number(next, token) + number, token = self.rule_number(next_val_fn, token) numbers.append(number) return numbers, token - def rule_6numbers(self, next, token): + def rule_6numbers(self, next_val_fn, token): numbers = [] - token = next() + token = next_val_fn() # all numbers are mandatory - for i in xrange(6): - number, token = self.rule_number(next, token) + for i in range(6): + number, token = self.rule_number(next_val_fn, token) numbers.append(number) return numbers, token - def rule_number(self, next, token): + def rule_number(self, next_val_fn, token): if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = Decimal(token[1]) * 1 - token = next() + token = next_val_fn() return x, token - def rule_optional_number(self, next, token): + def rule_optional_number(self, next_val_fn, token): if token[0] not in self.number_tokens: return None, token else: x = Decimal(token[1]) * 1 - token = next() + token = next_val_fn() return x, token diff --git a/setup.py b/setup.py index 4802ba1..1ced1ee 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup ( author_email = 'codedread@gmail.com', url = 'https://github.com/oberstet/scour', platforms = ('Any'), - install_requires = [], + install_requires = ['six>=1.9.0'], packages = find_packages(), zip_safe = True, entry_points = { From a5b09e382429e222ba3409455a3a817f2e40d7ff Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 17 Oct 2015 17:29:47 +0200 Subject: [PATCH 010/270] add notice of repo move --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 711de61..4e58ba5 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,4 @@ Packaging from [sources](http://www.codedread.com/scour/) retrieved on 2013/20/2 * done by Tavendo GmbH, Tobias Oberstein * license same as upstream (Apache 2.0) + * now official repo, please see [here](https://github.com/codedread/scour/issues/11) From 18266ca1ec8ed6d74b7bd4bb81ec7e2f2ddf79b7 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 15 Nov 2015 17:58:08 +0100 Subject: [PATCH 011/270] Add an option to set number of spaces (or tabs used for indentation) --- scour/scour.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index 34e571d..a97cfa9 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2765,6 +2765,7 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): I='' if options.indent_type == 'tab': I='\t' elif options.indent_type == 'space': I=' ' + I *= options.indent_depth outParts.extend([(I * ind), '<', element.nodeName]) @@ -3164,6 +3165,9 @@ _options_parser.add_option("-q", "--quiet", _options_parser.add_option("--indent", action="store", type="string", dest="indent_type", default="space", help="indentation of the output: none, space, tab (default: %default)") +_options_parser.add_option("--nindent", + action="store", type=int, dest="indent_depth", default=1, + help="depth of the indentation, i.e. number of spaces/tabs: (default: %default)") _options_parser.add_option("--protect-ids-noninkscape", action="store_true", dest="protect_ids_noninkscape", default=False, help="Don't change IDs not ending with a digit") @@ -3193,6 +3197,8 @@ def parse_args(args=None, ignore_additional_args=False): _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") From e21e36235346b509829143e097fe4fceabfef113 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 15 Nov 2015 18:44:17 +0100 Subject: [PATCH 012/270] Add an option to suppress output of line breaks (obviously this also disables indentation) --- scour/scour.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index a97cfa9..fa83d3a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2763,9 +2763,12 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): indent = ind I='' - if options.indent_type == 'tab': I='\t' - elif options.indent_type == 'space': I=' ' - I *= options.indent_depth + 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]) @@ -2824,7 +2827,7 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): if preserveWhitespace: outParts.append(serializeXML(child, options, 0, preserveWhitespace)) else: - outParts.extend(['\n', serializeXML(child, options, indent + 1, preserveWhitespace)]) + outParts.extend([newline, serializeXML(child, options, indent + 1, preserveWhitespace)]) onNewLine = True # text node elif child.nodeType == 3: @@ -2846,10 +2849,10 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): if onNewLine: outParts.append(I * ind) outParts.extend(['']) - if indent > 0: outParts.append('\n') + if indent > 0: outParts.append(newline) else: outParts.append('/>') - if indent > 0: outParts.append('\n') + if indent > 0: outParts.append(newline) return "".join(outParts) @@ -3168,6 +3171,9 @@ _options_parser.add_option("--indent", _options_parser.add_option("--nindent", action="store", type=int, dest="indent_depth", default=1, help="depth of the indentation, i.e. number of spaces/tabs: (default: %default)") +_options_parser.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\")") _options_parser.add_option("--protect-ids-noninkscape", action="store_true", dest="protect_ids_noninkscape", default=False, help="Don't change IDs not ending with a digit") From 7e36be4aaadbdfe7922423600354a674ece88b40 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 15 Nov 2015 19:26:39 +0100 Subject: [PATCH 013/270] Add an option to strip the xml:space="preserve" attribute from the root SVG element This attribute is added by at least one popular vector graphics editor thwarting our efforts to pretty-print the output. --- scour/scour.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index fa83d3a..5747483 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2922,6 +2922,9 @@ def scourString(in_string, options=None): if options.strip_comments: numCommentsRemoved = removeComments(doc) + if options.strip_xml_space_attribute and doc.documentElement.hasAttribute('xml:space'): + doc.documentElement.removeAttribute('xml:space') + # repair style (remove unnecessary style properties and change them into XML attributes) numStylePropsFixed = repairStyle(doc.documentElement, options) @@ -3174,6 +3177,9 @@ _options_parser.add_option("--nindent", _options_parser.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\")") +_options_parser.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") _options_parser.add_option("--protect-ids-noninkscape", action="store_true", dest="protect_ids_noninkscape", default=False, help="Don't change IDs not ending with a digit") From e903475289b42a6c97b14432de865639c4b04a90 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Mon, 16 Nov 2015 18:05:35 +0100 Subject: [PATCH 014/270] cleanups; bump version --- Makefile | 6 ++++++ scour/__init__.py | 4 +++- scour/scour.py | 8 +++++--- setup.py | 13 ++++++++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2fb7802..3585231 100644 --- a/Makefile +++ b/Makefile @@ -13,3 +13,9 @@ publish: clean python setup.py sdist upload python setup.py bdist_egg upload python setup.py bdist_wininst upload + +test_version: + PYTHONPATH=. python -m scour.scour --version + +test_help: + PYTHONPATH=. python -m scour.scour --help diff --git a/scour/__init__.py b/scour/__init__.py index 1282886..7a305fc 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -1,6 +1,6 @@ ############################################################################### ## -## Copyright (C) 2013 Tavendo GmbH +## 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. @@ -15,3 +15,5 @@ ## limitations under the License. ## ############################################################################### + +__version__ = u'0.31' diff --git a/scour/scour.py b/scour/scour.py index 5747483..978d45d 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -76,9 +76,11 @@ try: except ImportError: pass -APP = 'scour' -VER = '0.30' -COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2010' +from scour import __version__ + +APP = u'scour' +VER = __version__ +COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' NS = {'SVG': 'http://www.w3.org/2000/svg', 'XLINK': 'http://www.w3.org/1999/xlink', diff --git a/setup.py b/setup.py index 1ced1ee..cb38584 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ ## ############################################################################### +import re from setuptools import setup, find_packages LONGDESC = """ @@ -30,9 +31,19 @@ Authors: - Tobias Oberstein (maintainer) """ +VERSIONFILE = "scour/__init__.py" +verstrline = open(VERSIONFILE, "rt").read() +VSRE = r"^__version__ = u['\"]([^'\"]*)['\"]" +mo = re.search(VSRE, verstrline, re.M) +if mo: + verstr = mo.group(1) +else: + raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) + + setup ( name = 'scour', - version = '0.30', + version = verstr, description = 'Scour SVG Optimizer', # long_description = open("README.md").read(), long_description = LONGDESC, From ec7385c995ea5d79a5a39747839f065adb05869d Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Mon, 16 Nov 2015 18:08:02 +0100 Subject: [PATCH 015/270] update readme --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cb38584..cfb066a 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ Scour is a SVG optimizer/sanitizer that can be used to produce SVGs for Web depl Website - http://www.codedread.com/scour/ (original website) - - https://github.com/oberstet/scour (today) + - https://github.com/codedread/scour (today) Authors: - Jeff Schiller, Louis Simard (original authors) From d3b29a68da5a4a236e2f4dde3f4c65a6e74074c0 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Mon, 16 Nov 2015 18:12:56 +0100 Subject: [PATCH 016/270] fix links; only upload source dist --- Makefile | 2 -- setup.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3585231..bc870ee 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,6 @@ clean: publish: clean python setup.py register python setup.py sdist upload - python setup.py bdist_egg upload - python setup.py bdist_wininst upload test_version: PYTHONPATH=. python -m scour.scour --version diff --git a/setup.py b/setup.py index cfb066a..73f7e6a 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ setup ( license = 'Apache License 2.0', author = 'Jeff Schiller', author_email = 'codedread@gmail.com', - url = 'https://github.com/oberstet/scour', + url = 'https://github.com/codedread/scour', platforms = ('Any'), install_requires = ['six>=1.9.0'], packages = find_packages(), From 302f7f7477193165b2b1bdcd5997ee4e1acbf997 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Mon, 16 Nov 2015 20:36:12 +0100 Subject: [PATCH 017/270] use absolute imports --- scour/scour.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 978d45d..21f25c4 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -56,10 +56,10 @@ import sys import xml.dom.minidom import re import math -from .svg_regex import svg_parser -from .svg_transform import svg_transform_parser +from scour.svg_regex import svg_parser +from scour.svg_transform import svg_transform_parser import optparse -from .yocto_css import parseCssString +from scour.yocto_css import parseCssString import six from six.moves import range From b979fe19e563a559d03f15da818f95ddff00bc56 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 17 Nov 2015 22:30:23 +0100 Subject: [PATCH 018/270] Remove unused XML namespace declarations --- scour/scour.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 978d45d..010f686 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2903,7 +2903,17 @@ def scourString(in_string, options=None): doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') # TODO: throw error or warning? - # check for redundant SVG namespace declaration + # 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 attrName in six.iterkeys(element.attributes): + if attrName.startswith(prefix): + return False + return True + attrList = doc.documentElement.attributes xmlnsDeclsToRemove = [] redundantPrefixes = [] @@ -2911,9 +2921,12 @@ def scourString(in_string, options=None): attr = attrList.item(i) name = attr.nodeName val = attr.nodeValue - if name[0:6] == 'xmlns:' and val == 'http://www.w3.org/2000/svg': - redundantPrefixes.append(name[6:]) - xmlnsDeclsToRemove.append(name) + 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) From 67bacc2f2322ad340fb6d8346bea0a6df74a857e Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 17 Nov 2015 22:46:50 +0100 Subject: [PATCH 019/270] Remove metadata right away (i.e. before checking for unused XML namespaces; otherwise the relevant namespaces used for metadata would not yet be unused) --- scour/scour.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 010f686..f72e459 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2877,6 +2877,10 @@ def scourString(in_string, options=None): global numBytesSavedInTransforms doc = xml.dom.minidom.parseString(in_string) + # remove if the user wants to + if options.remove_metadata: + removeMetadataElements(doc) + # 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? @@ -2947,10 +2951,6 @@ def scourString(in_string, options=None): if options.simple_colors: numBytesSavedInColors = convertColors(doc.documentElement) - # remove if the user wants to - if options.remove_metadata: - removeMetadataElements(doc) - # remove unreferenced gradients/patterns outside of defs # and most unreferenced elements inside of defs while removeUnreferencedElements(doc, options.keep_defs) > 0: From 89f4b687f273f1181e1884b911f66f672ce3adc8 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 17 Nov 2015 22:49:50 +0100 Subject: [PATCH 020/270] Statistics: count also attributes removed from root SVG element --- scour/scour.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index f72e459..aeb730a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2934,6 +2934,7 @@ def scourString(in_string, options=None): for attrName in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attrName) + numAttrsRemoved += 1 for prefix in redundantPrefixes: remapNamespacePrefix(doc.documentElement, prefix, '') @@ -2943,6 +2944,7 @@ def scourString(in_string, options=None): 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) From 532f6640017c28ccb6e2067b969ded3ef313fb86 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 17 Nov 2015 23:09:25 +0100 Subject: [PATCH 021/270] Minor fix in console output (superfluous space) --- scour/scour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index aeb730a..19e8ac4 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3301,8 +3301,8 @@ def start(options, input, output): # GZ: not using globals would be good too if not options.quiet: - print(' File:', input.name, \ - os.linesep + ' Time taken:', str(end-start) + 's' + os.linesep, \ + print(' File:', input.name, os.linesep + \ + ' Time taken:', str(end-start) + 's', os.linesep + \ getReport(), file=sys.stderr) oldsize = len(in_string) From 6254548582ba77215321b5d8d5d2462dbac8f0da Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sat, 21 Nov 2015 23:29:59 +0100 Subject: [PATCH 022/270] Compatibility fix for Python 2.6 (NamedNodeList seems not to have had the iterkeys method back then) --- scour/scour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 62fd911..4ad70e6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2913,8 +2913,8 @@ def scourString(in_string, options=None): return False else: for element in doc.getElementsByTagName("*"): - for attrName in six.iterkeys(element.attributes): - if attrName.startswith(prefix): + for attribute in element.attributes.values(): + if attribute.name.startswith(prefix): return False return True From 3b7e8a00915266c891762caef4c0e6fb0a51abc8 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 6 Dec 2015 19:59:06 +0100 Subject: [PATCH 023/270] Restore unittests from history --- testcss.py | 47 + testscour.py | 1370 +++++++++++++++++ unittests/adobe.svg | 45 + .../cascading-default-attribute-removal.svg | 23 + unittests/cdata.svg | 6 + .../collapse-gradients-gradientUnits.svg | 11 + unittests/collapse-gradients.svg | 11 + unittests/collapse-same-path-points.svg | 4 + unittests/color-formats.svg | 12 + unittests/comment-beside-xml-decl.svg | 10 + unittests/comments.svg | 6 + unittests/commonized-referenced-elements.svg | 9 + unittests/consecutive-hlines.svg | 6 + unittests/css-reference.svg | 27 + unittests/doctype.svg | 7 + unittests/dont-collapse-gradients.svg | 13 + unittests/dont-convert-short-color-names.svg | 4 + unittests/duplicate-gradient-stops-pct.svg | 12 + unittests/duplicate-gradient-stops.svg | 19 + .../duplicate-gradients-update-style.svg | 15 + unittests/empty-g.svg | 7 + unittests/empty-metadata.svg | 3 + unittests/empty-style.svg | 4 + unittests/fill-none.svg | 5 + unittests/font-styles.svg | 4 + unittests/full-metadata.svg | 22 + unittests/gradient-default-attrs.svg | 10 + unittests/group-creation.svg | 6 + unittests/group-no-creation.svg | 6 + unittests/groups-in-switch-with-id.svg | 18 + unittests/groups-in-switch.svg | 18 + unittests/groups-with-title-desc.svg | 13 + unittests/ids-to-strip.svg | 11 + unittests/important-groups-in-defs.svg | 12 + unittests/inkscape.svg | 7 + unittests/metadata-with-text.svg | 4 + .../move-common-attributes-to-grandparent.svg | 10 + .../move-common-attributes-to-parent.svg | 13 + unittests/nested-defs.svg | 14 + unittests/nested-useless-groups.svg | 9 + unittests/no-collapse-lines.svg | 8 + unittests/overflow-marker.svg | 12 + unittests/overflow-svg.svg | 9 + unittests/path-abs-to-rel.svg | 4 + unittests/path-bez-optimize.svg | 4 + unittests/path-elliptical-arc-parsing.svg | 4 + unittests/path-empty-move.svg | 5 + unittests/path-implicit-line.svg | 4 + unittests/path-line-optimize.svg | 4 + unittests/path-precision.svg | 4 + unittests/path-quad-optimize.svg | 4 + unittests/path-simple-triangle.svg | 8 + unittests/path-sn.svg | 4 + unittests/path-truncate-zeros-calc.svg | 4 + unittests/path-truncate-zeros.svg | 4 + unittests/path-use-scientific-notation.svg | 4 + unittests/path-with-caps.svg | 4 + unittests/path-with-closepath.svg | 4 + unittests/polygon-coord-neg-first.svg | 4 + unittests/polygon-coord-neg.svg | 4 + unittests/polygon-coord.svg | 4 + unittests/polygon.svg | 5 + unittests/polyline-coord-neg-first.svg | 4 + unittests/polyline-coord-neg.svg | 4 + unittests/polyline-coord.svg | 4 + unittests/protection.svg | 11 + unittests/quot-in-url.svg | 10 + unittests/redundant-svg-namespace.svg | 8 + unittests/referenced-elements-1.svg | 11 + unittests/referenced-font.svg | 17 + unittests/refs-in-defs.svg | 8 + unittests/remove-duplicate-gradients.svg | 23 + .../remove-unused-attributes-on-parent.svg | 8 + unittests/scour-lengths.svg | 5 + unittests/shorten-ids.svg | 10 + unittests/sodipodi.svg | 7 + unittests/straight-curve.svg | 4 + unittests/stroke-none.svg | 4 + unittests/stroke-nowidth.svg | 4 + unittests/stroke-transparent.svg | 4 + unittests/style-cdata.svg | 16 + unittests/style-to-attr.svg | 9 + unittests/transform-matrix-is-identity.svg | 3 + unittests/transform-matrix-is-rotate-135.svg | 4 + unittests/transform-matrix-is-rotate-225.svg | 4 + unittests/transform-matrix-is-rotate-45.svg | 4 + unittests/transform-matrix-is-rotate-90.svg | 4 + .../transform-matrix-is-rotate-neg-45.svg | 4 + .../transform-matrix-is-rotate-neg-90.svg | 4 + unittests/transform-matrix-is-scale-2-3.svg | 3 + unittests/transform-matrix-is-scale-neg-1.svg | 4 + unittests/transform-matrix-is-translate.svg | 3 + unittests/transform-rotate-fold-3args.svg | 5 + unittests/transform-rotate-is-identity.svg | 3 + .../transform-rotate-trim-range-719.5.svg | 5 + .../transform-rotate-trim-range-neg-540.0.svg | 5 + unittests/transform-skewX-is-identity.svg | 4 + unittests/transform-skewY-is-identity.svg | 4 + unittests/transform-translate-is-identity.svg | 5 + unittests/unreferenced-font.svg | 17 + unittests/unreferenced-linearGradient.svg | 6 + unittests/unreferenced-pattern.svg | 6 + unittests/unreferenced-radialGradient.svg | 6 + unittests/useless-defs.svg | 21 + unittests/utf8.svg | 5 + unittests/whitespace-defs.svg | 6 + unittests/whitespace-important.svg | 4 + unittests/whitespace-nested.svg | 4 + unittests/xml-namespace-attrs.svg | 24 + unittests/xml-ns-decl.svg | 30 + unittests/xml-well-formed.svg | 9 + 111 files changed, 2341 insertions(+) create mode 100755 testcss.py create mode 100755 testscour.py create mode 100644 unittests/adobe.svg create mode 100644 unittests/cascading-default-attribute-removal.svg create mode 100644 unittests/cdata.svg create mode 100644 unittests/collapse-gradients-gradientUnits.svg create mode 100644 unittests/collapse-gradients.svg create mode 100644 unittests/collapse-same-path-points.svg create mode 100644 unittests/color-formats.svg create mode 100644 unittests/comment-beside-xml-decl.svg create mode 100644 unittests/comments.svg create mode 100644 unittests/commonized-referenced-elements.svg create mode 100644 unittests/consecutive-hlines.svg create mode 100644 unittests/css-reference.svg create mode 100644 unittests/doctype.svg create mode 100644 unittests/dont-collapse-gradients.svg create mode 100644 unittests/dont-convert-short-color-names.svg create mode 100644 unittests/duplicate-gradient-stops-pct.svg create mode 100644 unittests/duplicate-gradient-stops.svg create mode 100644 unittests/duplicate-gradients-update-style.svg create mode 100644 unittests/empty-g.svg create mode 100644 unittests/empty-metadata.svg create mode 100644 unittests/empty-style.svg create mode 100644 unittests/fill-none.svg create mode 100644 unittests/font-styles.svg create mode 100644 unittests/full-metadata.svg create mode 100644 unittests/gradient-default-attrs.svg create mode 100644 unittests/group-creation.svg create mode 100644 unittests/group-no-creation.svg create mode 100644 unittests/groups-in-switch-with-id.svg create mode 100644 unittests/groups-in-switch.svg create mode 100644 unittests/groups-with-title-desc.svg create mode 100644 unittests/ids-to-strip.svg create mode 100644 unittests/important-groups-in-defs.svg create mode 100644 unittests/inkscape.svg create mode 100644 unittests/metadata-with-text.svg create mode 100644 unittests/move-common-attributes-to-grandparent.svg create mode 100644 unittests/move-common-attributes-to-parent.svg create mode 100644 unittests/nested-defs.svg create mode 100644 unittests/nested-useless-groups.svg create mode 100644 unittests/no-collapse-lines.svg create mode 100644 unittests/overflow-marker.svg create mode 100644 unittests/overflow-svg.svg create mode 100644 unittests/path-abs-to-rel.svg create mode 100644 unittests/path-bez-optimize.svg create mode 100644 unittests/path-elliptical-arc-parsing.svg create mode 100644 unittests/path-empty-move.svg create mode 100644 unittests/path-implicit-line.svg create mode 100644 unittests/path-line-optimize.svg create mode 100644 unittests/path-precision.svg create mode 100644 unittests/path-quad-optimize.svg create mode 100644 unittests/path-simple-triangle.svg create mode 100644 unittests/path-sn.svg create mode 100644 unittests/path-truncate-zeros-calc.svg create mode 100644 unittests/path-truncate-zeros.svg create mode 100644 unittests/path-use-scientific-notation.svg create mode 100644 unittests/path-with-caps.svg create mode 100644 unittests/path-with-closepath.svg create mode 100644 unittests/polygon-coord-neg-first.svg create mode 100644 unittests/polygon-coord-neg.svg create mode 100644 unittests/polygon-coord.svg create mode 100644 unittests/polygon.svg create mode 100644 unittests/polyline-coord-neg-first.svg create mode 100644 unittests/polyline-coord-neg.svg create mode 100644 unittests/polyline-coord.svg create mode 100644 unittests/protection.svg create mode 100644 unittests/quot-in-url.svg create mode 100644 unittests/redundant-svg-namespace.svg create mode 100644 unittests/referenced-elements-1.svg create mode 100644 unittests/referenced-font.svg create mode 100644 unittests/refs-in-defs.svg create mode 100644 unittests/remove-duplicate-gradients.svg create mode 100644 unittests/remove-unused-attributes-on-parent.svg create mode 100644 unittests/scour-lengths.svg create mode 100644 unittests/shorten-ids.svg create mode 100644 unittests/sodipodi.svg create mode 100644 unittests/straight-curve.svg create mode 100644 unittests/stroke-none.svg create mode 100644 unittests/stroke-nowidth.svg create mode 100644 unittests/stroke-transparent.svg create mode 100644 unittests/style-cdata.svg create mode 100644 unittests/style-to-attr.svg create mode 100644 unittests/transform-matrix-is-identity.svg create mode 100644 unittests/transform-matrix-is-rotate-135.svg create mode 100644 unittests/transform-matrix-is-rotate-225.svg create mode 100644 unittests/transform-matrix-is-rotate-45.svg create mode 100644 unittests/transform-matrix-is-rotate-90.svg create mode 100644 unittests/transform-matrix-is-rotate-neg-45.svg create mode 100644 unittests/transform-matrix-is-rotate-neg-90.svg create mode 100644 unittests/transform-matrix-is-scale-2-3.svg create mode 100644 unittests/transform-matrix-is-scale-neg-1.svg create mode 100644 unittests/transform-matrix-is-translate.svg create mode 100644 unittests/transform-rotate-fold-3args.svg create mode 100644 unittests/transform-rotate-is-identity.svg create mode 100644 unittests/transform-rotate-trim-range-719.5.svg create mode 100644 unittests/transform-rotate-trim-range-neg-540.0.svg create mode 100644 unittests/transform-skewX-is-identity.svg create mode 100644 unittests/transform-skewY-is-identity.svg create mode 100644 unittests/transform-translate-is-identity.svg create mode 100644 unittests/unreferenced-font.svg create mode 100644 unittests/unreferenced-linearGradient.svg create mode 100644 unittests/unreferenced-pattern.svg create mode 100644 unittests/unreferenced-radialGradient.svg create mode 100644 unittests/useless-defs.svg create mode 100644 unittests/utf8.svg create mode 100644 unittests/whitespace-defs.svg create mode 100644 unittests/whitespace-important.svg create mode 100644 unittests/whitespace-nested.svg create mode 100644 unittests/xml-namespace-attrs.svg create mode 100644 unittests/xml-ns-decl.svg create mode 100644 unittests/xml-well-formed.svg diff --git a/testcss.py b/testcss.py new file mode 100755 index 0000000..243ab35 --- /dev/null +++ b/testcss.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Test Harness for Scour +# +# Copyright 2010 Jeff Schiller +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from yocto_css import parseCssString + +class Blank(unittest.TestCase): + def runTest(self): + r = parseCssString('') + self.assertEquals( len(r), 0, 'Blank string returned non-empty list') + self.assertEquals( type(r), type([]), 'Blank string returned non list') + +class ElementSelector(unittest.TestCase): + def runTest(self): + r = parseCssString('foo {}') + self.assertEquals( len(r), 1, 'Element selector not returned') + self.assertEquals( r[0]['selector'], 'foo', 'Selector for foo not returned') + self.assertEquals( len(r[0]['properties']), 0, 'Property list for foo not empty') + +class ElementSelectorWithProperty(unittest.TestCase): + def runTest(self): + r = parseCssString('foo { bar: baz}') + self.assertEquals( len(r), 1, 'Element selector not returned') + self.assertEquals( r[0]['selector'], 'foo', 'Selector for foo not returned') + self.assertEquals( len(r[0]['properties']), 1, 'Property list for foo did not have 1') + self.assertEquals( 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 new file mode 100755 index 0000000..0167875 --- /dev/null +++ b/testscour.py @@ -0,0 +1,1370 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Test Harness for Scour +# +# Copyright 2010 Jeff Schiller +# Copyright 2010 Louis Simard +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import xml.dom.minidom +from svg_regex import svg_parser +from 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) == False: return False + for child in elem.childNodes: + if walkTree(child, func) == False: return False + return True + +class ScourOptions: + simple_colors = True + style_to_xml = True + group_collapse = True + strip_ids = False + digits = 5 + embed_rasters = True + keep_editor_data = False + strip_xml_prolog = False + indent_type = "space" + enable_viewboxing = False + shorten_ids = False + strip_comments = False + remove_metadata = False + group_create = False + +class NoInkscapeElements(unittest.TestCase): + def runTest(self): + self.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), False, + 'Found Adobe XPath elements' ) + +class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/metadata-with-text.svg') + self.assertEquals(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-metadata.svg') + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, + 'Unreferenced rect left in defs' ) + +class DoNotRemoveChainedRefsInDefs(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/refs-in-defs.svg') + g = doc.getElementsByTagNameNS(SVGNS, 'g')[0] + self.assertEquals( 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.assertEquals(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.assertEquals(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.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', + ' 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'])[0]) + self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), '', + ' ID not stripped' ) + +class RemoveUselessNestedGroups(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/nested-useless-groups.svg') + self.assertEquals(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'])[0]) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertNotEquals(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.assertEquals(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.assertEquals(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.assertEquals(len(fonts), 1, + 'Font wrongly removed from ' ) + +class ConvertStyleToAttrs(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', + 'stroke attribute not emptied when no stroke' ) + +class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', + 'stroke-width attribute not emptied when no stroke' ) + +class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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'])[0]) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertNotEquals(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.assertEquals(path[:4] == 'm300' and path[4] != '.', True, + 'Trailing zeros not removed from path data' ) + +class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/path-truncate-zeros-calc.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEquals(path, 'm5.81 0h0.1', + '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.assertEquals(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.assertEquals(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.assertEquals(path[1][0], 'v', + 'Absolute V command not converted to relative v command') + self.assertEquals(float(path[1][1][0]), -20.0, + 'Absolute V value not converted to relative v value') + +class RoundPathData(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/path-precision.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEquals(float(path[0][1][0]), 100.0, + 'Not rounding down' ) + self.assertEquals(float(path[0][1][1]), 100.0, + 'Not rounding up' ) + +class LimitPrecisionInPathData(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/path-precision.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(path[1][0], 'h', + 'Did not change line to horizontal line segment in path' ) + self.assertEquals(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.assertEquals(path[2][0], 'v', + 'Did not change line to vertical line segment in path' ) + self.assertEquals(float(path[2][1][0]), 100.0, + 'Did not calculate vertical line segment in path correctly' ) + +class ChangeBezierToShorthandInPath(unittest.TestCase): + def runTest(self): + path = scour.scourXmlFile('unittests/path-bez-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEquals(path.getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', + 'Did not change bezier curves into shorthand curve segments in path') + +class ChangeQuadToShorthandInPath(unittest.TestCase): + def runTest(self): + path = scour.scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEquals(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', + 'Did not change quadratic curves into shorthand curve segments in path') + +class HandleNonAsciiUtf8(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/utf8.svg') + desc = unicode(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() + self.assertEquals( desc, u'Ăş', + 'Did not handle non-ASCII characters' ) + +class HandleSciNoInPathData(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/path-sn.svg') + self.assertEquals( 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.assertEquals( elem.getAttribute('fill'), '#0f1011', + 'Not converting rgb into hex') + +class TranslateRGBPctIntoHex(unittest.TestCase): + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0] + self.assertEquals( elem.getAttribute('stop-color'), '#7f0000', + 'Not converting rgb pct into hex') + +class TranslateColorNamesIntoHex(unittest.TestCase): + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEquals( elem.getAttribute('stroke'), '#a9a9a9', + 'Not converting standard color names into hex') + +class TranslateExtendedColorNamesIntoHex(unittest.TestCase): + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0] + self.assertEquals( elem.getAttribute('solid-color'), '#fafad2', + 'Not converting extended color names into hex') + +class TranslateLongHexColorIntoShortHex(unittest.TestCase): + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0] + self.assertEquals( elem.getAttribute('fill'), '#fff', + '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.assertEquals( '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.assertEquals( 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.assertEquals( 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.assertEquals( p.getAttribute('d'), 'm100 100h200v100h-200z', + 'Did not collapse consecutive hlines segments') + +class CollapseConsecutiveHLinesCoords(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[1] + self.assertEquals( p.getAttribute('d'), 'm100 300h200v100h-200z', + 'Did not collapse consecutive hlines coordinates') + +class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[2] + self.assertEquals( p.getAttribute('d'), 'm100 500h300-100v100h-200z', + 'Collapsed consecutive hlines segments with differing signs') + +class ConvertStraightCurvesToLines(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEquals(p.getAttribute('d'), 'm10 10l40 40 40-40z', + 'Did not convert straight curves into lines') + +class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + self.assertEquals(p.getAttribute('points'), '50 50 150 50 150 150 50 150', + 'Unnecessary polygon end point not removed' ) + +class DoNotRemovePolgonLastPoint(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1] + self.assertEquals(p.getAttribute('points'), '200 50 300 50 300 150 200 150', + 'Last point of polygon removed' ) + +class ScourPolygonCoordsSciNo(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + self.assertEquals(p.getAttribute('points'), '1e4 50', + 'Polygon coordinates not scoured') + +class ScourPolylineCoordsSciNo(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEquals(p.getAttribute('points'), '1e4 50', + 'Polyline coordinates not scoured') + +class ScourPolygonNegativeCoords(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + # points="100,-100,100-100,100-100-100,-100-100,200" /> + self.assertEquals(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polygon coordinates not properly parsed') + +class ScourPolylineNegativeCoords(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEquals(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', + '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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), + 'Rect not changed after removing duplicate linear gradient') + self.assertEquals(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), + 'Rect not changed 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.assertEquals(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.assertEquals(rects[2].getAttribute('stroke'), rects[3].getAttribute('fill'), + 'Rect not changed after removing duplicate radial gradient') + +class CollapseSamePathPoints(unittest.TestCase): + def runTest(self): + p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0]; + self.assertEquals(p.getAttribute('d'), "m100 100l100.12 100.12c14.88 4.88-15.12-5.12 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.assertEquals(svg.getAttribute('x'), '1', + 'Did not scour x attribute of svg element with unitless number') + self.assertEquals(r.getAttribute('x'), '123.46', + 'Did not scour x attribute of rect with unitless number') + self.assertEquals(r.getAttribute('y'), '123', + 'Did not scour y attribute of rect unitless number') + self.assertEquals(r.getAttribute('width'), '300', + 'Did not scour width attribute of rect with unitless number') + self.assertEquals(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.assertEquals(r.getAttribute('x'), '123.46px', + 'Did not scour x attribute with unit') + self.assertEquals(r.getAttribute('y'), '35ex', + 'Did not scour y attribute with unit') + self.assertEquals(r.getAttribute('width'), '300pt', + 'Did not scour width attribute with unit') + self.assertEquals(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.assertNotEquals( 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.assertEquals( r.tagName, 'rect', + 'Redundant svg: prefix not removed') + + +class RemoveDefaultGradX1Value(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[0] + self.assertEquals( g.getAttribute('x1'), '', + 'x1="0" not removed') + +class RemoveDefaultGradY1Value(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[0] + self.assertEquals( g.getAttribute('y1'), '', + 'y1="0" not removed') + +class RemoveDefaultGradX2Value(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[0] + self.assertEquals( g.getAttribute('x2'), '', + 'x2="100%" not removed') + +class RemoveDefaultGradY2Value(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[0] + self.assertEquals( g.getAttribute('y2'), '', + 'y2="0" not removed') + +class RemoveDefaultGradGradientUnitsValue(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[0] + self.assertEquals( g.getAttribute('gradientUnits'), '', + 'gradientUnits="objectBoundingBox" not removed') + +class RemoveDefaultGradSpreadMethodValue(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[0] + self.assertEquals( g.getAttribute('spreadMethod'), '', + 'spreadMethod="pad" not removed') + +class RemoveDefaultGradCXValue(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'radialGradient')[0] + self.assertEquals( g.getAttribute('cx'), '', + 'cx="50%" not removed') + +class RemoveDefaultGradCYValue(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'radialGradient')[0] + self.assertEquals( g.getAttribute('cy'), '', + 'cy="50%" not removed') + +class RemoveDefaultGradRValue(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'radialGradient')[0] + self.assertEquals( g.getAttribute('r'), '', + 'r="50%" not removed') + +class RemoveDefaultGradFXValue(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'radialGradient')[0] + self.assertEquals( g.getAttribute('fx'), '', + 'fx matching cx not removed') + +class RemoveDefaultGradFYValue(unittest.TestCase): + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementsByTagNameNS(SVGNS, 'radialGradient')[0] + self.assertEquals( g.getAttribute('fy'), '', + 'fy matching cy not removed') + +class CDATAInXml(unittest.TestCase): + def runTest(self): + lines = scour.scourString(open('unittests/cdata.svg').read()).splitlines() + self.assertEquals( lines[3], + " alert('pb&j');", + 'CDATA did not come out correctly') + +class WellFormedXMLLesserThanInAttrValue(unittest.TestCase): + def runTest(self): + wellformed = scour.scourString(open('unittests/xml-well-formed.svg').read()) + self.assert_( wellformed.find('unicode="<"') != -1, + "Improperly serialized < in attribute value") + +class WellFormedXMLAmpersandInAttrValue(unittest.TestCase): + def runTest(self): + wellformed = scour.scourString(open('unittests/xml-well-formed.svg').read()) + self.assert_( wellformed.find('unicode="&"') != -1, + 'Improperly serialized & in attribute value' ) + +class WellFormedXMLLesserThanInTextContent(unittest.TestCase): + def runTest(self): + wellformed = scour.scourString(open('unittests/xml-well-formed.svg').read()) + self.assert_( wellformed.find('2 < 5') != -1, + 'Improperly serialized < in text content') + +class WellFormedXMLAmpersandInTextContent(unittest.TestCase): + def runTest(self): + wellformed = scour.scourString(open('unittests/xml-well-formed.svg').read()) + self.assert_( wellformed.find('Peanut Butter & Jelly') != -1, + 'Improperly serialized & in text content') + +class WellFormedXMLNamespacePrefix(unittest.TestCase): + def runTest(self): + wellformed = scour.scourString(open('unittests/xml-well-formed.svg').read()) + self.assert_( wellformed.find('xmlns:foo=') != -1, + 'Improperly serialized namespace prefix declarations') + +class NamespaceDeclPrefixesInXMLWhenNotInDefaultNamespace(unittest.TestCase): + def runTest(self): + xmlstring = scour.scourString(open('unittests/xml-ns-decl.svg').read()) + self.assert_( 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): + xmlstring = scour.scourString(open('unittests/xml-ns-decl.svg').read()) + self.assert_( xmlstring.find(' + + This is some messed-up markup + +'''.splitlines() + for i in range(4): + self.assertEquals( s[i], c[i], + 'Whitespace not preserved for line ' + str(i)) + +class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): + def runTest(self): + s = scour.scourString(open('unittests/whitespace-nested.svg').read()).splitlines() + c = ''' + + Use bold text + +'''.splitlines() + for i in range(4): + self.assertEquals( 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.assertEquals( grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', + 'Did not get xlink:href prefix right') + +class EnsurePreserveWhitespaceOnNonTextElements(unittest.TestCase): + def runTest(self): + s = scour.scourString(open('unittests/no-collapse-lines.svg').read()) + self.assertEquals( 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.assertEquals( fail, False, + 'Could not handle an empty style element') + +class EnsureLineEndings(unittest.TestCase): + def runTest(self): + s = scour.scourString(open('unittests/whitespace-important.svg').read()) + self.assertEquals( len(s.splitlines()), 4, + 'Did not output line ending character correctly') + +class XmlEntities(unittest.TestCase): + def runTest(self): + self.assertEquals( scour.makeWellFormed('<>&"\''), '<>&"'', + 'Incorrectly translated XML entities') + +class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/comments.svg') + self.assertEquals( doc.childNodes.length, 4, + 'Did not include all comment children outside of root') + self.assertEquals( doc.childNodes[0].nodeType, 8, 'First node not a comment') + self.assertEquals( doc.childNodes[1].nodeType, 8, 'Second node not a comment') + self.assertEquals( doc.childNodes[3].nodeType, 8, 'Fourth node not a comment') + +class DoNotStripDoctype(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/doctype.svg') + self.assertEquals( doc.childNodes.length, 3, + 'Did not include the DOCROOT') + self.assertEquals( doc.childNodes[0].nodeType, 8, 'First node not a comment') + self.assertEquals( doc.childNodes[1].nodeType, 10, 'Second node not a doctype') + self.assertEquals( 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.assertEquals( path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100", + "Implicit line segments after move not preserved") + +class RemoveMetadataOption(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/full-metadata.svg', + scour.parse_args(['--remove-metadata'])[0]) + self.assertEquals(doc.childNodes.length, 1, + 'Did not remove tag with --remove-metadata') + +class EnableCommentStrippingOption(unittest.TestCase): + def runTest(self): + docStr = file('unittests/comment-beside-xml-decl.svg').read() + docStr = scour.scourString(docStr, + scour.parse_args(['--enable-comment-stripping'])[0]) + self.assertEquals(docStr.find(' + + + + + + diff --git a/unittests/comments.svg b/unittests/comments.svg new file mode 100644 index 0000000..06a75f2 --- /dev/null +++ b/unittests/comments.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/commonized-referenced-elements.svg b/unittests/commonized-referenced-elements.svg new file mode 100644 index 0000000..3a152fb --- /dev/null +++ b/unittests/commonized-referenced-elements.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/unittests/consecutive-hlines.svg b/unittests/consecutive-hlines.svg new file mode 100644 index 0000000..caae623 --- /dev/null +++ b/unittests/consecutive-hlines.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/css-reference.svg b/unittests/css-reference.svg new file mode 100644 index 0000000..6330c60 --- /dev/null +++ b/unittests/css-reference.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/unittests/doctype.svg b/unittests/doctype.svg new file mode 100644 index 0000000..d19e074 --- /dev/null +++ b/unittests/doctype.svg @@ -0,0 +1,7 @@ + + + + +]> + diff --git a/unittests/dont-collapse-gradients.svg b/unittests/dont-collapse-gradients.svg new file mode 100644 index 0000000..00b58f5 --- /dev/null +++ b/unittests/dont-collapse-gradients.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/unittests/dont-convert-short-color-names.svg b/unittests/dont-convert-short-color-names.svg new file mode 100644 index 0000000..cbcece7 --- /dev/null +++ b/unittests/dont-convert-short-color-names.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/duplicate-gradient-stops-pct.svg b/unittests/duplicate-gradient-stops-pct.svg new file mode 100644 index 0000000..43c99c4 --- /dev/null +++ b/unittests/duplicate-gradient-stops-pct.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/unittests/duplicate-gradient-stops.svg b/unittests/duplicate-gradient-stops.svg new file mode 100644 index 0000000..4629bd6 --- /dev/null +++ b/unittests/duplicate-gradient-stops.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/unittests/duplicate-gradients-update-style.svg b/unittests/duplicate-gradients-update-style.svg new file mode 100644 index 0000000..c28070c --- /dev/null +++ b/unittests/duplicate-gradients-update-style.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/unittests/empty-g.svg b/unittests/empty-g.svg new file mode 100644 index 0000000..ccb7355 --- /dev/null +++ b/unittests/empty-g.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/unittests/empty-metadata.svg b/unittests/empty-metadata.svg new file mode 100644 index 0000000..ca3c31f --- /dev/null +++ b/unittests/empty-metadata.svg @@ -0,0 +1,3 @@ + + + diff --git a/unittests/empty-style.svg b/unittests/empty-style.svg new file mode 100644 index 0000000..a2d2afd --- /dev/null +++ b/unittests/empty-style.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/fill-none.svg b/unittests/fill-none.svg new file mode 100644 index 0000000..6442c90 --- /dev/null +++ b/unittests/fill-none.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/font-styles.svg b/unittests/font-styles.svg new file mode 100644 index 0000000..e4120df --- /dev/null +++ b/unittests/font-styles.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/full-metadata.svg b/unittests/full-metadata.svg new file mode 100644 index 0000000..f67e01d --- /dev/null +++ b/unittests/full-metadata.svg @@ -0,0 +1,22 @@ + + + + + + + No One + + + + + + diff --git a/unittests/gradient-default-attrs.svg b/unittests/gradient-default-attrs.svg new file mode 100644 index 0000000..36fd0e7 --- /dev/null +++ b/unittests/gradient-default-attrs.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/unittests/group-creation.svg b/unittests/group-creation.svg new file mode 100644 index 0000000..96776c0 --- /dev/null +++ b/unittests/group-creation.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/group-no-creation.svg b/unittests/group-no-creation.svg new file mode 100644 index 0000000..bea6419 --- /dev/null +++ b/unittests/group-no-creation.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/groups-in-switch-with-id.svg b/unittests/groups-in-switch-with-id.svg new file mode 100644 index 0000000..317cfcc --- /dev/null +++ b/unittests/groups-in-switch-with-id.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/unittests/groups-in-switch.svg b/unittests/groups-in-switch.svg new file mode 100644 index 0000000..96394fd --- /dev/null +++ b/unittests/groups-in-switch.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/unittests/groups-with-title-desc.svg b/unittests/groups-with-title-desc.svg new file mode 100644 index 0000000..7983dc0 --- /dev/null +++ b/unittests/groups-with-title-desc.svg @@ -0,0 +1,13 @@ + + + + Group 1 + + + + + Group 1 + + + + diff --git a/unittests/ids-to-strip.svg b/unittests/ids-to-strip.svg new file mode 100644 index 0000000..1ac59bc --- /dev/null +++ b/unittests/ids-to-strip.svg @@ -0,0 +1,11 @@ + + + + Fooey + + + + + + + diff --git a/unittests/important-groups-in-defs.svg b/unittests/important-groups-in-defs.svg new file mode 100644 index 0000000..18ba1df --- /dev/null +++ b/unittests/important-groups-in-defs.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/unittests/inkscape.svg b/unittests/inkscape.svg new file mode 100644 index 0000000..a51ad49 --- /dev/null +++ b/unittests/inkscape.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/unittests/metadata-with-text.svg b/unittests/metadata-with-text.svg new file mode 100644 index 0000000..6149b68 --- /dev/null +++ b/unittests/metadata-with-text.svg @@ -0,0 +1,4 @@ + + + This is a metadata element with only text node children + diff --git a/unittests/move-common-attributes-to-grandparent.svg b/unittests/move-common-attributes-to-grandparent.svg new file mode 100644 index 0000000..4e202bd --- /dev/null +++ b/unittests/move-common-attributes-to-grandparent.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/unittests/move-common-attributes-to-parent.svg b/unittests/move-common-attributes-to-parent.svg new file mode 100644 index 0000000..f390c89 --- /dev/null +++ b/unittests/move-common-attributes-to-parent.svg @@ -0,0 +1,13 @@ + + + + + + + +Hello +World! +Goodbye +Cruel World! + + diff --git a/unittests/nested-defs.svg b/unittests/nested-defs.svg new file mode 100644 index 0000000..7091985 --- /dev/null +++ b/unittests/nested-defs.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/unittests/nested-useless-groups.svg b/unittests/nested-useless-groups.svg new file mode 100644 index 0000000..73b5f88 --- /dev/null +++ b/unittests/nested-useless-groups.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/unittests/no-collapse-lines.svg b/unittests/no-collapse-lines.svg new file mode 100644 index 0000000..85da385 --- /dev/null +++ b/unittests/no-collapse-lines.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/unittests/overflow-marker.svg b/unittests/overflow-marker.svg new file mode 100644 index 0000000..ec068d9 --- /dev/null +++ b/unittests/overflow-marker.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/unittests/overflow-svg.svg b/unittests/overflow-svg.svg new file mode 100644 index 0000000..8830a80 --- /dev/null +++ b/unittests/overflow-svg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/unittests/path-abs-to-rel.svg b/unittests/path-abs-to-rel.svg new file mode 100644 index 0000000..c9cc803 --- /dev/null +++ b/unittests/path-abs-to-rel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-bez-optimize.svg b/unittests/path-bez-optimize.svg new file mode 100644 index 0000000..97bfdd1 --- /dev/null +++ b/unittests/path-bez-optimize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-elliptical-arc-parsing.svg b/unittests/path-elliptical-arc-parsing.svg new file mode 100644 index 0000000..77a8cbd --- /dev/null +++ b/unittests/path-elliptical-arc-parsing.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-empty-move.svg b/unittests/path-empty-move.svg new file mode 100644 index 0000000..d3b63d7 --- /dev/null +++ b/unittests/path-empty-move.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/path-implicit-line.svg b/unittests/path-implicit-line.svg new file mode 100644 index 0000000..a42848e --- /dev/null +++ b/unittests/path-implicit-line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-line-optimize.svg b/unittests/path-line-optimize.svg new file mode 100644 index 0000000..13cc139 --- /dev/null +++ b/unittests/path-line-optimize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-precision.svg b/unittests/path-precision.svg new file mode 100644 index 0000000..8e1e267 --- /dev/null +++ b/unittests/path-precision.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-quad-optimize.svg b/unittests/path-quad-optimize.svg new file mode 100644 index 0000000..bbe3bc9 --- /dev/null +++ b/unittests/path-quad-optimize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-simple-triangle.svg b/unittests/path-simple-triangle.svg new file mode 100644 index 0000000..94ab17e --- /dev/null +++ b/unittests/path-simple-triangle.svg @@ -0,0 +1,8 @@ + + + + diff --git a/unittests/path-sn.svg b/unittests/path-sn.svg new file mode 100644 index 0000000..0b9f7d2 --- /dev/null +++ b/unittests/path-sn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-truncate-zeros-calc.svg b/unittests/path-truncate-zeros-calc.svg new file mode 100644 index 0000000..c889fff --- /dev/null +++ b/unittests/path-truncate-zeros-calc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-truncate-zeros.svg b/unittests/path-truncate-zeros.svg new file mode 100644 index 0000000..ad1c6d5 --- /dev/null +++ b/unittests/path-truncate-zeros.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-use-scientific-notation.svg b/unittests/path-use-scientific-notation.svg new file mode 100644 index 0000000..afbbf05 --- /dev/null +++ b/unittests/path-use-scientific-notation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-with-caps.svg b/unittests/path-with-caps.svg new file mode 100644 index 0000000..0e7ab1a --- /dev/null +++ b/unittests/path-with-caps.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/path-with-closepath.svg b/unittests/path-with-closepath.svg new file mode 100644 index 0000000..80858ca --- /dev/null +++ b/unittests/path-with-closepath.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/polygon-coord-neg-first.svg b/unittests/polygon-coord-neg-first.svg new file mode 100644 index 0000000..9f87a3e --- /dev/null +++ b/unittests/polygon-coord-neg-first.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/polygon-coord-neg.svg b/unittests/polygon-coord-neg.svg new file mode 100644 index 0000000..73fe0b9 --- /dev/null +++ b/unittests/polygon-coord-neg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/polygon-coord.svg b/unittests/polygon-coord.svg new file mode 100644 index 0000000..15940d4 --- /dev/null +++ b/unittests/polygon-coord.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/polygon.svg b/unittests/polygon.svg new file mode 100644 index 0000000..d927a00 --- /dev/null +++ b/unittests/polygon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/polyline-coord-neg-first.svg b/unittests/polyline-coord-neg-first.svg new file mode 100644 index 0000000..41d1981 --- /dev/null +++ b/unittests/polyline-coord-neg-first.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/polyline-coord-neg.svg b/unittests/polyline-coord-neg.svg new file mode 100644 index 0000000..da82dad --- /dev/null +++ b/unittests/polyline-coord-neg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/polyline-coord.svg b/unittests/polyline-coord.svg new file mode 100644 index 0000000..fc209ed --- /dev/null +++ b/unittests/polyline-coord.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/protection.svg b/unittests/protection.svg new file mode 100644 index 0000000..f2930f5 --- /dev/null +++ b/unittests/protection.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/unittests/quot-in-url.svg b/unittests/quot-in-url.svg new file mode 100644 index 0000000..6d82567 --- /dev/null +++ b/unittests/quot-in-url.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/unittests/redundant-svg-namespace.svg b/unittests/redundant-svg-namespace.svg new file mode 100644 index 0000000..5022693 --- /dev/null +++ b/unittests/redundant-svg-namespace.svg @@ -0,0 +1,8 @@ + + + + + Test + + + diff --git a/unittests/referenced-elements-1.svg b/unittests/referenced-elements-1.svg new file mode 100644 index 0000000..e779080 --- /dev/null +++ b/unittests/referenced-elements-1.svg @@ -0,0 +1,11 @@ + + + + Fooey + + + + + + + diff --git a/unittests/referenced-font.svg b/unittests/referenced-font.svg new file mode 100644 index 0000000..7d992ec --- /dev/null +++ b/unittests/referenced-font.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + Text + diff --git a/unittests/refs-in-defs.svg b/unittests/refs-in-defs.svg new file mode 100644 index 0000000..8636c5a --- /dev/null +++ b/unittests/refs-in-defs.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/unittests/remove-duplicate-gradients.svg b/unittests/remove-duplicate-gradients.svg new file mode 100644 index 0000000..f986bdd --- /dev/null +++ b/unittests/remove-duplicate-gradients.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/unittests/remove-unused-attributes-on-parent.svg b/unittests/remove-unused-attributes-on-parent.svg new file mode 100644 index 0000000..7f68d15 --- /dev/null +++ b/unittests/remove-unused-attributes-on-parent.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/unittests/scour-lengths.svg b/unittests/scour-lengths.svg new file mode 100644 index 0000000..f5c0d5c --- /dev/null +++ b/unittests/scour-lengths.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/shorten-ids.svg b/unittests/shorten-ids.svg new file mode 100644 index 0000000..7852c57 --- /dev/null +++ b/unittests/shorten-ids.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/unittests/sodipodi.svg b/unittests/sodipodi.svg new file mode 100644 index 0000000..935884a --- /dev/null +++ b/unittests/sodipodi.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/unittests/straight-curve.svg b/unittests/straight-curve.svg new file mode 100644 index 0000000..95cd862 --- /dev/null +++ b/unittests/straight-curve.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/stroke-none.svg b/unittests/stroke-none.svg new file mode 100644 index 0000000..4582a85 --- /dev/null +++ b/unittests/stroke-none.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/stroke-nowidth.svg b/unittests/stroke-nowidth.svg new file mode 100644 index 0000000..2ca5809 --- /dev/null +++ b/unittests/stroke-nowidth.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/stroke-transparent.svg b/unittests/stroke-transparent.svg new file mode 100644 index 0000000..4ff39a2 --- /dev/null +++ b/unittests/stroke-transparent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/style-cdata.svg b/unittests/style-cdata.svg new file mode 100644 index 0000000..4740da9 --- /dev/null +++ b/unittests/style-cdata.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/unittests/style-to-attr.svg b/unittests/style-to-attr.svg new file mode 100644 index 0000000..3bbe3a0 --- /dev/null +++ b/unittests/style-to-attr.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/unittests/transform-matrix-is-identity.svg b/unittests/transform-matrix-is-identity.svg new file mode 100644 index 0000000..9764b28 --- /dev/null +++ b/unittests/transform-matrix-is-identity.svg @@ -0,0 +1,3 @@ + + + diff --git a/unittests/transform-matrix-is-rotate-135.svg b/unittests/transform-matrix-is-rotate-135.svg new file mode 100644 index 0000000..a0583bc --- /dev/null +++ b/unittests/transform-matrix-is-rotate-135.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-matrix-is-rotate-225.svg b/unittests/transform-matrix-is-rotate-225.svg new file mode 100644 index 0000000..1aa21ef --- /dev/null +++ b/unittests/transform-matrix-is-rotate-225.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-matrix-is-rotate-45.svg b/unittests/transform-matrix-is-rotate-45.svg new file mode 100644 index 0000000..1749d98 --- /dev/null +++ b/unittests/transform-matrix-is-rotate-45.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-matrix-is-rotate-90.svg b/unittests/transform-matrix-is-rotate-90.svg new file mode 100644 index 0000000..269d526 --- /dev/null +++ b/unittests/transform-matrix-is-rotate-90.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-matrix-is-rotate-neg-45.svg b/unittests/transform-matrix-is-rotate-neg-45.svg new file mode 100644 index 0000000..37b46e8 --- /dev/null +++ b/unittests/transform-matrix-is-rotate-neg-45.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-matrix-is-rotate-neg-90.svg b/unittests/transform-matrix-is-rotate-neg-90.svg new file mode 100644 index 0000000..8fbbd4f --- /dev/null +++ b/unittests/transform-matrix-is-rotate-neg-90.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-matrix-is-scale-2-3.svg b/unittests/transform-matrix-is-scale-2-3.svg new file mode 100644 index 0000000..7a04ce5 --- /dev/null +++ b/unittests/transform-matrix-is-scale-2-3.svg @@ -0,0 +1,3 @@ + + + diff --git a/unittests/transform-matrix-is-scale-neg-1.svg b/unittests/transform-matrix-is-scale-neg-1.svg new file mode 100644 index 0000000..d402058 --- /dev/null +++ b/unittests/transform-matrix-is-scale-neg-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-matrix-is-translate.svg b/unittests/transform-matrix-is-translate.svg new file mode 100644 index 0000000..0dfcd9d --- /dev/null +++ b/unittests/transform-matrix-is-translate.svg @@ -0,0 +1,3 @@ + + + diff --git a/unittests/transform-rotate-fold-3args.svg b/unittests/transform-rotate-fold-3args.svg new file mode 100644 index 0000000..0139610 --- /dev/null +++ b/unittests/transform-rotate-fold-3args.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/transform-rotate-is-identity.svg b/unittests/transform-rotate-is-identity.svg new file mode 100644 index 0000000..198ba11 --- /dev/null +++ b/unittests/transform-rotate-is-identity.svg @@ -0,0 +1,3 @@ + + + diff --git a/unittests/transform-rotate-trim-range-719.5.svg b/unittests/transform-rotate-trim-range-719.5.svg new file mode 100644 index 0000000..f0bb947 --- /dev/null +++ b/unittests/transform-rotate-trim-range-719.5.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/transform-rotate-trim-range-neg-540.0.svg b/unittests/transform-rotate-trim-range-neg-540.0.svg new file mode 100644 index 0000000..3e857f6 --- /dev/null +++ b/unittests/transform-rotate-trim-range-neg-540.0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/transform-skewX-is-identity.svg b/unittests/transform-skewX-is-identity.svg new file mode 100644 index 0000000..b038c6e --- /dev/null +++ b/unittests/transform-skewX-is-identity.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-skewY-is-identity.svg b/unittests/transform-skewY-is-identity.svg new file mode 100644 index 0000000..27da015 --- /dev/null +++ b/unittests/transform-skewY-is-identity.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unittests/transform-translate-is-identity.svg b/unittests/transform-translate-is-identity.svg new file mode 100644 index 0000000..6c62d23 --- /dev/null +++ b/unittests/transform-translate-is-identity.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unittests/unreferenced-font.svg b/unittests/unreferenced-font.svg new file mode 100644 index 0000000..560c83f --- /dev/null +++ b/unittests/unreferenced-font.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + Text + diff --git a/unittests/unreferenced-linearGradient.svg b/unittests/unreferenced-linearGradient.svg new file mode 100644 index 0000000..f588eac --- /dev/null +++ b/unittests/unreferenced-linearGradient.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/unreferenced-pattern.svg b/unittests/unreferenced-pattern.svg new file mode 100644 index 0000000..7bcff58 --- /dev/null +++ b/unittests/unreferenced-pattern.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/unreferenced-radialGradient.svg b/unittests/unreferenced-radialGradient.svg new file mode 100644 index 0000000..bfa35c8 --- /dev/null +++ b/unittests/unreferenced-radialGradient.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/useless-defs.svg b/unittests/useless-defs.svg new file mode 100644 index 0000000..f4663ff --- /dev/null +++ b/unittests/useless-defs.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/unittests/utf8.svg b/unittests/utf8.svg new file mode 100644 index 0000000..6c77d7a --- /dev/null +++ b/unittests/utf8.svg @@ -0,0 +1,5 @@ + + + Ăş + diff --git a/unittests/whitespace-defs.svg b/unittests/whitespace-defs.svg new file mode 100644 index 0000000..a32fcb4 --- /dev/null +++ b/unittests/whitespace-defs.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/unittests/whitespace-important.svg b/unittests/whitespace-important.svg new file mode 100644 index 0000000..6918044 --- /dev/null +++ b/unittests/whitespace-important.svg @@ -0,0 +1,4 @@ + + + This is some messed-up markup + diff --git a/unittests/whitespace-nested.svg b/unittests/whitespace-nested.svg new file mode 100644 index 0000000..3b99356 --- /dev/null +++ b/unittests/whitespace-nested.svg @@ -0,0 +1,4 @@ + + + Use bold text + diff --git a/unittests/xml-namespace-attrs.svg b/unittests/xml-namespace-attrs.svg new file mode 100644 index 0000000..81c5fb4 --- /dev/null +++ b/unittests/xml-namespace-attrs.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/unittests/xml-ns-decl.svg b/unittests/xml-ns-decl.svg new file mode 100644 index 0000000..0f057a7 --- /dev/null +++ b/unittests/xml-ns-decl.svg @@ -0,0 +1,30 @@ + + + + + image/svg+xml + + Open Clip Art Logo + 10-01-2004 + + + Andreas Nilsson + + + + + + Jon Phillips, Tobias Jakobs + + + This is one version of the official Open Clip Art Library logo. + logo, open clip art library logo, logotype + + + + + + + + + diff --git a/unittests/xml-well-formed.svg b/unittests/xml-well-formed.svg new file mode 100644 index 0000000..705a288 --- /dev/null +++ b/unittests/xml-well-formed.svg @@ -0,0 +1,9 @@ + + + + 2 < 5 + Peanut Butter & Jelly + + + ΉTML & CSS + From 391ff77659b119682d68e10aabc3f90caaa073be Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 6 Dec 2015 20:10:12 +0100 Subject: [PATCH 024/270] Unittests: Fix import paths --- testcss.py | 2 +- testscour.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testcss.py b/testcss.py index 243ab35..70601e0 100755 --- a/testcss.py +++ b/testcss.py @@ -20,7 +20,7 @@ # limitations under the License. import unittest -from yocto_css import parseCssString +from scour.yocto_css import parseCssString class Blank(unittest.TestCase): def runTest(self): diff --git a/testscour.py b/testscour.py index 0167875..9f7a5b3 100755 --- a/testscour.py +++ b/testscour.py @@ -22,8 +22,8 @@ import unittest import xml.dom.minidom -from svg_regex import svg_parser -from scour import scourXmlFile, scourString, parse_args, makeWellFormed +from scour.svg_regex import svg_parser +from scour.scour import scourXmlFile, scourString, parse_args, makeWellFormed SVGNS = 'http://www.w3.org/2000/svg' From 320bdda6e979e340fe159f3565b0f6fd977f9022 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 6 Dec 2015 21:20:05 +0100 Subject: [PATCH 025/270] Unittests: Run python-modernize --- testcss.py | 4 ++++ testscour.py | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/testcss.py b/testcss.py index 70601e0..a46144e 100755 --- a/testcss.py +++ b/testcss.py @@ -19,9 +19,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import + import unittest + from scour.yocto_css import parseCssString + class Blank(unittest.TestCase): def runTest(self): r = parseCssString('') diff --git a/testscour.py b/testscour.py index 9f7a5b3..4fbdef3 100755 --- a/testscour.py +++ b/testscour.py @@ -20,11 +20,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import + +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 + SVGNS = 'http://www.w3.org/2000/svg' # I couldn't figure out how to get ElementTree to work with the following XPath @@ -591,7 +598,7 @@ class ChangeQuadToShorthandInPath(unittest.TestCase): class HandleNonAsciiUtf8(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/utf8.svg') - desc = unicode(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() self.assertEquals( desc, u'Ăş', 'Did not handle non-ASCII characters' ) @@ -1066,7 +1073,7 @@ class RemoveMetadataOption(unittest.TestCase): class EnableCommentStrippingOption(unittest.TestCase): def runTest(self): - docStr = file('unittests/comment-beside-xml-decl.svg').read() + docStr = open('unittests/comment-beside-xml-decl.svg').read() docStr = scour.scourString(docStr, scour.parse_args(['--enable-comment-stripping'])[0]) self.assertEquals(docStr.find(' 2 < 5 Peanut Butter & Jelly + + ΉTML & CSS From 4eade6920145b5953a42e4bbb14deda81dafa090 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 8 Dec 2015 23:38:06 +0100 Subject: [PATCH 031/270] Open input file in binary mode an let XML parser deal with encoding. Fixes #26 --- scour/scour.py | 4 ++-- testscour.py | 12 +++++++++--- unittests/utf8.svg | 20 +++++++++++++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 335bd0e..a62ee58 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3097,7 +3097,7 @@ def scourString(in_string, options=None): # input is a filename # returns the minidom doc representation of the SVG def scourXmlFile(filename, options=None): - with open(filename) as f: + with open(filename, "rb") as f: in_string = f.read() out_string = scourString(in_string, options) return xml.dom.minidom.parseString(out_string.encode('utf-8')) @@ -3235,7 +3235,7 @@ def parse_args(args=None, ignore_additional_args=False): _options_parser.error("Input filename is the same as output filename") if options.infilename: - infile = maybe_gziped_file(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 diff --git a/testscour.py b/testscour.py index 7b29bdf..8912288 100755 --- a/testscour.py +++ b/testscour.py @@ -604,12 +604,18 @@ class ChangeQuadToShorthandInPath(unittest.TestCase): self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', 'Did not change quadratic curves into shorthand curve segments in path') -class HandleNonAsciiUtf8(unittest.TestCase): +class HandleUTF8(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/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, u'Ăş', - 'Did not handle non-ASCII 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' ) + 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 HandleSciNoInPathData(unittest.TestCase): def runTest(self): diff --git a/unittests/utf8.svg b/unittests/utf8.svg index 6c77d7a..dd63f12 100644 --- a/unittests/utf8.svg +++ b/unittests/utf8.svg @@ -1,5 +1,19 @@ - - Ăş + + Hello in many languages: +ar: أهلا +bn: হ্যালো +el: ΧαίĎετε +en: Hello +hi: नमस्ते +iw: שלום +ja: ă“ă‚“ă«ăˇăŻ +km: ជំរាបសួរ +ml: ഹലോ +ru: ЗдравŃтвŃйте +ur: Űیل٠+zh: 您好 + “”â€â€™â€“—…â€â€’°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿� + :-×÷±âžĎ€â…≤≥≠â‰â§â¨â©âŞââ€ââ„â‘âŹâ†â†‘→↓↔↕↖↗â†â†™â†şâ†»â‡’⇔ + â°ÂąÂ˛Âłâ´âµâ¶â·â¸âąâşâ»â˝âľâżâ±â‚€â‚â‚‚â‚₄₅₆₇â‚₉₊₋₌₍₎ From 8984e550b07df5e490ebcac4194ee87fc6927844 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Wed, 9 Dec 2015 00:30:16 +0100 Subject: [PATCH 032/270] Read from stdin in binary mode an let XML parser deal with encoding. Also write to stdout in binary mode as the output is already encoded. --- scour/scour.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index a62ee58..cb2d386 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3239,11 +3239,20 @@ def parse_args(args=None, ignore_additional_args=False): # GZ: could catch a raised IOError here and report else: # GZ: could sniff for gzip compression here - infile = sys.stdin + # + # open the binary buffer of stdin and let XML parser handle decoding + try: + infile = sys.stdin.buffer + except AttributeError: + infile = sys.stdin if options.outfilename: outfile = maybe_gziped_file(options.outfilename, "wb") else: - outfile = sys.stdout + # open the binary buffer of stdout as the output is already encoded + try: + outfile = sys.stdout.buffer + except AttributeError: + outfile = sys.stdout return options, [infile, outfile] From 946ca3ce4acc7a21337c696cd83b0ad4af05f40f Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Wed, 9 Dec 2015 21:31:16 +0100 Subject: [PATCH 033/270] Unittests: Add a test for proper decoding of ISO 8859-15 --- testscour.py | 10 ++++++++-- unittests/encoding-iso-8859-15.svg | 4 ++++ unittests/{utf8.svg => encoding-utf8.svg} | 0 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 unittests/encoding-iso-8859-15.svg rename unittests/{utf8.svg => encoding-utf8.svg} (100%) diff --git a/testscour.py b/testscour.py index 8912288..a5c8185 100755 --- a/testscour.py +++ b/testscour.py @@ -604,9 +604,9 @@ class ChangeQuadToShorthandInPath(unittest.TestCase): self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', 'Did not change quadratic curves into shorthand curve segments in path') -class HandleUTF8(unittest.TestCase): +class HandleEncodingUTF8(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/utf8.svg') + 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' ) @@ -617,6 +617,12 @@ class HandleUTF8(unittest.TestCase): 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' ) + class HandleSciNoInPathData(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/path-sn.svg') diff --git a/unittests/encoding-iso-8859-15.svg b/unittests/encoding-iso-8859-15.svg new file mode 100644 index 0000000..626aca4 --- /dev/null +++ b/unittests/encoding-iso-8859-15.svg @@ -0,0 +1,4 @@ + + + áčîäöüߤ¦¨´¸Ľ˝ľ + diff --git a/unittests/utf8.svg b/unittests/encoding-utf8.svg similarity index 100% rename from unittests/utf8.svg rename to unittests/encoding-utf8.svg From c698522c28b5bf61ad4bd0c1e42452255e59b5ef Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Thu, 10 Dec 2015 22:50:31 +0100 Subject: [PATCH 034/270] Simplify and fix "removeComments()" * The separate treatment of comments at the documentElement's level is not necessary - they have a parent (as tested in Python 3.5.0 and 2.7.11 and 2.6.6)! It might not have worked before due to a typo - note the "if isinstance(element,...)" and "len(element.data)" which should both refer to "subelement" instead - or a bug in very old versions of Python). * Fix the iteration over childNodes (i.e. replace "for subelement in element.childNodes:" with ""for subelement in element.childNodes[:]:". We have to create a copy of the list to iterate over, otherwise we'd be iterating over a list as we change it which leads to unpredictable results. Fixes #25 --- scour/scour.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 335bd0e..d4a4628 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2587,20 +2587,11 @@ def removeComments(element) : """ global numCommentBytes - if isinstance(element, xml.dom.minidom.Document): - # must process the document object separately, because its - # documentElement's nodes have None as their parentNode - for subelement in element.childNodes: - if isinstance(element, xml.dom.minidom.Comment): - numCommentBytes += len(element.data) - element.documentElement.removeChild(subelement) - else: - removeComments(subelement) - elif isinstance(element, xml.dom.minidom.Comment): + if isinstance(element, xml.dom.minidom.Comment): numCommentBytes += len(element.data) element.parentNode.removeChild(element) else: - for subelement in element.childNodes: + for subelement in element.childNodes[:]: removeComments(subelement) From 232f27269af48be5259d835da3f77c8aa3c3f06c Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Thu, 10 Dec 2015 23:39:16 +0100 Subject: [PATCH 035/270] bump version for release --- scour/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/__init__.py b/scour/__init__.py index 7a305fc..fef9c03 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ ## ############################################################################### -__version__ = u'0.31' +__version__ = u'0.32' From 972812e6c70edcab68d02095a9e9a0faa51dea0a Mon Sep 17 00:00:00 2001 From: Eitot Date: Mon, 21 Dec 2015 08:16:14 +0100 Subject: [PATCH 036/270] Update README.md --- README.md | 91 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 4e58ba5..ebb5f4b 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,68 @@ # Scour +Scour is a Python module that takes an SVG file and produces a cleaner and more concise file. It is intended to be used after exporting with a GUI editor, such as Inkscape or Adobe Illustrator. -Scour is a Python module that takes an input SVG and outputs a cleaner, -more concise SVG file. The goal is that authors will use this script after -editing the file in a GUI editor such as Inkscape or Adobe Illustrator. +## Requirements +* [Python](https://www.python.org) 2.6 or later +* [six](https://pypi.python.org/pypi/six) 1.9 or later +* [psyco](https://pypi.python.org/pypi/psyco) (optional, Python 2.6 only) -Scour was started as a vehicle for me to learn Python. In addition, the goal -is to reduce the amount of time I spend in cleaning up files I find on sites -such as openclipart.org +## Installation +Scour can be installed manually or with a package manager, such as [pip](https://pip.pypa.io) or [Homebrew](http://brew.sh). It is also included as an Inkscape extension and in some Linux distributions. -Ideas are pulled from three places: +### Manual installation +Download Scour and six and locate the `setup.py` file in both packages. Open a console and enter the following commands: +``` +python /path/to/six/setup.py install +``` +``` +python /path/to/Scour/setup.py install +``` +Do the same if you want to use psyco. - * my head - * Sam Ruby's SVG Tidy script: http://intertwingly.net/code/svgtidy/svgtidy.rb - * Inkscape's proposal for a 'cleaned SVG': http://wiki.inkscape.org/wiki/index.php/Save_Cleaned_SVG +### Package manager +To install Scour using pip, enter the following command into a console: +``` +pip install scour +``` -Regards, +To do the same with Homebrew: +``` +brew install scour +``` -Jeff Schiller, 2009-04-06 - -codedread@gmail.com - -http://blog.codedread.com/ - -http://www.codedread.com/scour/ +Using pip or Homebrew will install six automatically (Homebrew will also install Python, if not installed). ## Usage - Standard: +``` +scour -i input.svg -o output.svg +``` +Better (for older versions of Internet Explorer): +``` +scour -i input.svg -o output.svg --enable-viewboxing +``` +Maximum scrubbing: +``` +scour -i input.svg -o output.svg --enable-viewboxing --enable-id-stripping \ + --enable-comment-stripping --shorten-ids --indent=none +``` +Maximum scrubbing and a compressed SVGZ file: +``` +scour -i input.svg -o output.svgz --enable-viewboxing --enable-id-stripping \ + --enable-comment-stripping --shorten-ids --indent=none +``` - scour -i mysvg.svg -o mysvg_opt.svg +## Licence +[Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE) -Better (this works in IE which needs Viewbox): +## Development +Scour was originally developed by Jeff "codedread" Schiller and Louis Simard. Development is [now maintained](https://github.com/codedread/scour/issues/11) by Tobias "oberstet" Oberstein. - scour -i mysvg.svg -o mysvg_opt.svg --enable-viewboxing +Scour was started as a vehicle for to learn Python. In addition, the goal was to reduce the amount of time spent in cleaning up files that are found on sites such as [openclipart.org](https://openclipart.org). -Maximum: +Ideas were pulled from three places: + * the original author's head + * Sam Ruby's [SVG Tidy script](http://intertwingly.net/code/svgtidy/svgtidy.rb) + * Inkscape's [proposal for a 'cleaned SVG'](http://wiki.inkscape.org/wiki/index.php/Save_Cleaned_SVG) - scour -i mysvg.svg -o mysvg_opt.svg --enable-viewboxing --enable-id-stripping \ - --enable-comment-stripping --shorten-ids --indent=none - -Maximum + Compress: - - scour -i mysvg.svg -o mysvg_opt.svgz --enable-viewboxing --enable-id-stripping \ - --enable-comment-stripping --shorten-ids --indent=none - -## Notes - -Packaging from [sources](http://www.codedread.com/scour/) retrieved on 2013/20/22: - - * done by Tavendo GmbH, Tobias Oberstein - * license same as upstream (Apache 2.0) - * now official repo, please see [here](https://github.com/codedread/scour/issues/11) +This Github repository is the official one. The official website as well as older packages can be found at [www.codedread.com/scour](http://www.codedread.com/scour/). From 184efee16e30196c2a029b2b0a04f59797eaac0a Mon Sep 17 00:00:00 2001 From: Eitot Date: Thu, 24 Dec 2015 11:18:08 +0100 Subject: [PATCH 037/270] - Initial support for Sketch - Typos in README.md --- README.md | 4 ++-- scour/scour.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ebb5f4b..91b6776 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,13 @@ scour -i input.svg -o output.svgz --enable-viewboxing --enable-id-stripping \ --enable-comment-stripping --shorten-ids --indent=none ``` -## Licence +## License [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE) ## Development Scour was originally developed by Jeff "codedread" Schiller and Louis Simard. Development is [now maintained](https://github.com/codedread/scour/issues/11) by Tobias "oberstet" Oberstein. -Scour was started as a vehicle for to learn Python. In addition, the goal was to reduce the amount of time spent in cleaning up files that are found on sites such as [openclipart.org](https://openclipart.org). +Scour was started as a vehicle for learning Python. In addition, the goal was to reduce the amount of time spent on cleaning up files that are found on sites such as [openclipart.org](https://openclipart.org). Ideas were pulled from three places: * the original author's head diff --git a/scour/scour.py b/scour/scour.py index 9283a8f..695a936 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -95,13 +95,15 @@ NS = {'SVG': 'http://www.w3.org/2000/svg', 'ADOBE_FLOWS': 'http://ns.adobe.com/Flows/1.0/', 'ADOBE_IMAGE_REPLACEMENT': 'http://ns.adobe.com/ImageReplacement/1.0/', 'ADOBE_CUSTOM': 'http://ns.adobe.com/GenericCustomNamespace/1.0/', - 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/' + 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/', + '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['ADOBE_IMAGE_REPLACEMENT'], NS['ADOBE_CUSTOM'], + NS['ADOBE_XPATH'], NS['SKETCH'] ] svgAttributes = [ 'clip-rule', @@ -3150,7 +3152,7 @@ _options_parser.add_option("--keep-unreferenced-defs", help="won't remove elements within the defs container that are unreferenced") _options_parser.add_option("--keep-editor-data", action="store_true", dest="keep_editor_data", default=False, - help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") + help="won't remove Inkscape, Sodipodi, Adobe Illustrator or Sketch elements and attributes") _options_parser.add_option("--remove-metadata", action="store_true", dest="remove_metadata", default=False, help="remove elements (which may contain license metadata etc.)") From f05d73b859094d573b8a3c7ea550d0e700bade57 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 15 Jan 2016 10:53:35 +0100 Subject: [PATCH 038/270] cleanup readme --- README.md | 65 ++++++++++++++++++------------------------------------- 1 file changed, 21 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 91b6776..6f59c8f 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,45 @@ # Scour -Scour is a Python module that takes an SVG file and produces a cleaner and more concise file. It is intended to be used after exporting with a GUI editor, such as Inkscape or Adobe Illustrator. -## Requirements -* [Python](https://www.python.org) 2.6 or later -* [six](https://pypi.python.org/pypi/six) 1.9 or later -* [psyco](https://pypi.python.org/pypi/psyco) (optional, Python 2.6 only) +Scour is a Python tool that takes an SVG file and produces a cleaner and more concise file. It is intended to be used **after** exporting to SVG with a GUI editor, such as Inkscape or Adobe Illustrator. + +Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). + +Scour was originally developed by Jeff "codedread" Schiller and Louis Simard. Development is [now maintained](https://github.com/codedread/scour/issues/11) by Tobias "oberstet" Oberstein. + +This Github repository is the official one. The official website as well as older packages can be found at [www.codedread.com/scour](http://www.codedread.com/scour/). ## Installation -Scour can be installed manually or with a package manager, such as [pip](https://pip.pypa.io) or [Homebrew](http://brew.sh). It is also included as an Inkscape extension and in some Linux distributions. -### Manual installation -Download Scour and six and locate the `setup.py` file in both packages. Open a console and enter the following commands: -``` -python /path/to/six/setup.py install -``` -``` -python /path/to/Scour/setup.py install -``` -Do the same if you want to use psyco. +Scour requires [Python](https://www.python.org) 2.6 or 2.7 (Python 3 currently does NOT work - see [here](https://github.com/codedread/scour/issues/30)). Further, for installation, use [pip](https://pip.pypa.io): -### Package manager -To install Scour using pip, enter the following command into a console: -``` +```console pip install scour ``` -To do the same with Homebrew: -``` -brew install scour -``` - -Using pip or Homebrew will install six automatically (Homebrew will also install Python, if not installed). - ## Usage + Standard: -``` + +```console scour -i input.svg -o output.svg ``` + Better (for older versions of Internet Explorer): -``` + +```console scour -i input.svg -o output.svg --enable-viewboxing ``` + Maximum scrubbing: -``` + +```console scour -i input.svg -o output.svg --enable-viewboxing --enable-id-stripping \ --enable-comment-stripping --shorten-ids --indent=none ``` + Maximum scrubbing and a compressed SVGZ file: -``` + +```console scour -i input.svg -o output.svgz --enable-viewboxing --enable-id-stripping \ --enable-comment-stripping --shorten-ids --indent=none ``` - -## License -[Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE) - -## Development -Scour was originally developed by Jeff "codedread" Schiller and Louis Simard. Development is [now maintained](https://github.com/codedread/scour/issues/11) by Tobias "oberstet" Oberstein. - -Scour was started as a vehicle for learning Python. In addition, the goal was to reduce the amount of time spent on cleaning up files that are found on sites such as [openclipart.org](https://openclipart.org). - -Ideas were pulled from three places: - * the original author's head - * Sam Ruby's [SVG Tidy script](http://intertwingly.net/code/svgtidy/svgtidy.rb) - * Inkscape's [proposal for a 'cleaned SVG'](http://wiki.inkscape.org/wiki/index.php/Save_Cleaned_SVG) - -This Github repository is the official one. The official website as well as older packages can be found at [www.codedread.com/scour](http://www.codedread.com/scour/). From 07e9ec02574cdeac48fd8207a10aadc8e07c8269 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 15 Jan 2016 11:07:29 +0100 Subject: [PATCH 039/270] add notes on how to install latest trunk --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f59c8f..48cbfed 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,20 @@ This Github repository is the official one. The official website as well as olde ## Installation -Scour requires [Python](https://www.python.org) 2.6 or 2.7 (Python 3 currently does NOT work - see [here](https://github.com/codedread/scour/issues/30)). Further, for installation, use [pip](https://pip.pypa.io): +Scour requires [Python](https://www.python.org) 2.6 or 2.7 (Python 3 currently does NOT work - see [here](https://github.com/codedread/scour/issues/30)). Further, for installation, [pip](https://pip.pypa.io) should be used. + +To install the [latest release](https://pypi.python.org/pypi/scour) of Scour from PyPI: ```console pip install scour ``` +To install the [latest trunk](https://github.com/codedread/scour) version (which might be broken!) from GitHub: + +```console +pip install https://github.com/codedread/scour/archive/master.zip +``` + ## Usage Standard: From 73ec7da13e668ae6555f20daa673c29659f390a4 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 15 Jan 2016 11:15:22 +0100 Subject: [PATCH 040/270] allow installation via direct execution of setup.py from outside the package directory --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 73f7e6a..c14779a 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ ## ############################################################################### +import os import re from setuptools import setup, find_packages @@ -31,7 +32,7 @@ Authors: - Tobias Oberstein (maintainer) """ -VERSIONFILE = "scour/__init__.py" +VERSIONFILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "scour", "__init__.py") verstrline = open(VERSIONFILE, "rt").read() VSRE = r"^__version__ = u['\"]([^'\"]*)['\"]" mo = re.search(VSRE, verstrline, re.M) From 939dd160bc73f59ed73c1345ee070267566b1904 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 15 Jan 2016 13:17:34 +0100 Subject: [PATCH 041/270] scour does indeed work on py3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48cbfed..4d4c237 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This Github repository is the official one. The official website as well as olde ## Installation -Scour requires [Python](https://www.python.org) 2.6 or 2.7 (Python 3 currently does NOT work - see [here](https://github.com/codedread/scour/issues/30)). Further, for installation, [pip](https://pip.pypa.io) should be used. +Scour requires [Python](https://www.python.org) 2.6, 2.7 or 3.3+. Further, for installation, [pip](https://pip.pypa.io) should be used. To install the [latest release](https://pypi.python.org/pypi/scour) of Scour from PyPI: From 170f8c7baacabf27b3038eb17fc6989aa31ea3fe Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 29 Jan 2016 09:25:43 +0100 Subject: [PATCH 042/270] turn down log noise; improve logging --- scour/scour.py | 53 ++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 695a936..ec5e5f8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -56,6 +56,7 @@ import sys import xml.dom.minidom import re import math +import time from scour.svg_regex import svg_parser from scour.svg_transform import svg_transform_parser import optparse @@ -67,7 +68,7 @@ from six.moves import range try: from decimal import Decimal, InvalidOperation, getcontext except ImportError: - print("Scour requires Python 2.4.", file=sys.stderr) + print("Scour requires at least Python 2.6+ or Python 3.3+.") # Import Psyco if available try: @@ -76,6 +77,12 @@ try: except ImportError: pass +# 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__ APP = u'scour' @@ -3181,6 +3188,9 @@ _options_parser.add_option("-o", _options_parser.add_option("-q", "--quiet", 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 log output (eg report processed elements and such)") _options_parser.add_option("--indent", action="store", type="string", dest="indent_type", default="space", help="indentation of the output: none, space, tab (default: %default)") @@ -3279,19 +3289,9 @@ def generateDefaultOptions(): return Struct(**d) - def start(options, input, output): - if sys.platform == "win32": - from time import clock as get_tick - else: - # GZ: is this different from time.time() in any way? - def get_tick(): - return os.times()[0] - start = get_tick() - - if not options.quiet: - print("%s %s\n%s" % (APP, VER, COPYRIGHT), file=sys.stderr) + start = walltime() # do the work in_string = input.read() @@ -3302,20 +3302,24 @@ def start(options, input, output): input.close() output.close() - end = get_tick() + end = walltime() + + # run-time in ms + duration = int(round((end - start) * 1000.)) + + oldsize = len(in_string) + newsize = len(out_string) + sizediff = (newsize / oldsize) * 100. - # GZ: not using globals would be good too if not options.quiet: - print(' File:', input.name, os.linesep + \ - ' Time taken:', str(end-start) + 's', os.linesep + \ - getReport(), file=sys.stderr) - - oldsize = len(in_string) - newsize = len(out_string) - sizediff = (newsize / oldsize) * 100 - print(' Original file size:', oldsize, 'bytes;', \ - 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)', file=sys.stderr) - + print('Scour processed file "{}" in {} ms: {}/{} bytes orig/new -> {:.1f}%'.format( + input.name, + duration, + oldsize, + newsize, + sizediff)) + if options.verbose: + print(getReport()) def run(): @@ -3323,6 +3327,5 @@ def run(): start(options, input, output) - if __name__ == '__main__': run() From 1a9d6119e045ca3541d7e43e2c6f84a124455158 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 29 Jan 2016 09:26:12 +0100 Subject: [PATCH 043/270] bump version --- scour/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/__init__.py b/scour/__init__.py index fef9c03..30d73cf 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ ## ############################################################################### -__version__ = u'0.32' +__version__ = u'0.33' From e701acdc25edf923a1169e877cb2255b398bc57d Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Fri, 19 Feb 2016 04:03:59 +0100 Subject: [PATCH 044/270] Add a mechanism to sanitize options. This simplifies usage of the Scour module while avoiding any compatibility issues that might be caused by adding/removing/renaming options. --- scour/scour.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index ec5e5f8..8ab6c81 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2866,8 +2866,9 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): # input is a string representation of the input XML # returns a string representation of the output XML def scourString(in_string, options=None): - if options is None: - options = _options_parser.get_default_values() + # sanitize options (take missing attributes from defaults, discard unknown attributes) + options = sanitizeOptions(options) + getcontext().prec = options.digits global numAttrsRemoved global numStylePropsFixed @@ -3289,6 +3290,18 @@ def generateDefaultOptions(): 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('__')) + + sanitizedOptions = _options_parser.get_default_values() + sanitizedOptions._update_careful(optionsDict) + + return sanitizedOptions + + + def start(options, input, output): start = walltime() From cf08a72e41aa6f9715424530009c113c6ba10978 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Fri, 19 Feb 2016 04:33:27 +0100 Subject: [PATCH 045/270] Unittests: Add two tests for and simplify after e701acdc25edf923a1169e877cb2255b398bc57d --- testscour.py | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/testscour.py b/testscour.py index a5c8185..9697cfe 100755 --- a/testscour.py +++ b/testscour.py @@ -45,30 +45,31 @@ def walkTree(elem, func): if walkTree(child, func) == False: return False return True + class ScourOptions: - simple_colors = True - style_to_xml = True - group_collapse = True - group_create = False - strip_ids = False - strip_comments = False - shorten_ids = False - shorten_ids_prefix = "" - embed_rasters = True - keep_defs = False - keep_editor_data = False - remove_metadata = False - renderer_workaround = True - strip_xml_prolog = False - enable_viewboxing = False - digits = 5 - indent_type = "space" - indent_depth = 1 - newlines = True - strip_xml_space_attribute = False - protect_ids_noninkscape = False - protect_ids_list = None - protect_ids_prefix = None + 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') + +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') class NoInkscapeElements(unittest.TestCase): def runTest(self): From b042c93b2c5872231ffc3793d76a52db8f3c4ffc Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Fri, 19 Feb 2016 04:47:33 +0100 Subject: [PATCH 046/270] Switch order of new/old size in console output (It already looked like a fraction, now it also yields the correct result if it's read like one) --- scour/scour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index ec5e5f8..0b2412a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3312,11 +3312,11 @@ def start(options, input, output): sizediff = (newsize / oldsize) * 100. if not options.quiet: - print('Scour processed file "{}" in {} ms: {}/{} bytes orig/new -> {:.1f}%'.format( + print('Scour processed file "{}" in {} ms: {}/{} bytes new/orig -> {:.1f}%'.format( input.name, duration, - oldsize, newsize, + oldsize, sizediff)) if options.verbose: print(getReport()) From 72c2ec8e1c788ce879155bc0defa9d2fe552cecf Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sat, 20 Feb 2016 17:51:49 +0100 Subject: [PATCH 047/270] Add possibility to specify input/output filename using positional arguments (e.g. 'scour input.svg output.svg') --- scour/scour.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 5942778..6958deb 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3120,11 +3120,10 @@ class HeaderedFormatter(optparse.IndentedHelpFormatter): # 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 [-i input.svg] [-o output.svg] [OPTIONS]", - description=("If the input/output files are specified with a svgz" - " extension, then compressed SVG is assumed. If the input file is not" - " specified, stdin is used. If the output file is not specified, " - " stdout is used."), + usage="%prog [input.svg [output.svg]] [OPTIONS]", + description=("If the input/output files are not specified, stdin/stdout is used. " + "If the input/output files are specified with a svgz extension, " + "then compressed SVG is assumed."), formatter=HeaderedFormatter(max_help_position=30), version=VER) @@ -3227,8 +3226,13 @@ def maybe_gziped_file(filename, mode="r"): def parse_args(args=None, ignore_additional_args=False): options, rargs = _options_parser.parse_args(args) - if rargs and not ignore_additional_args: - _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) + 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"]: From 77906518c08b8c35848567c1ff9e53f0dda57dcb Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sat, 20 Feb 2016 18:43:43 +0100 Subject: [PATCH 048/270] Use option groups for command line arguments to achieve clearer help output --- scour/scour.py | 166 ++++++++++++++++++++++++++----------------------- 1 file changed, 89 insertions(+), 77 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 6958deb..b861bc9 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3120,98 +3120,110 @@ class HeaderedFormatter(optparse.IndentedHelpFormatter): # 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 is used. " + 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=30), + formatter=HeaderedFormatter(max_help_position=33), version=VER) -_options_parser.add_option("--disable-simplify-colors", - action="store_false", dest="simple_colors", default=True, - help="won't convert all colors to #RRGGBB format") -_options_parser.add_option("--disable-style-to-xml", - action="store_false", dest="style_to_xml", default=True, - help="won't convert styles into XML attributes") -_options_parser.add_option("--disable-group-collapsing", - action="store_false", dest="group_collapse", default=True, - help="won't collapse elements") -_options_parser.add_option("--create-groups", - action="store_true", dest="group_create", default=False, - help="create elements for runs of elements with identical attributes") -_options_parser.add_option("--enable-id-stripping", - action="store_true", dest="strip_ids", default=False, - help="remove all un-referenced ID attributes") -_options_parser.add_option("--enable-comment-stripping", - action="store_true", dest="strip_comments", default=False, - help="remove all comments") -_options_parser.add_option("--shorten-ids", - action="store_true", dest="shorten_ids", default=False, - help="shorten all ID attributes to the least number of letters possible") -_options_parser.add_option("--shorten-ids-prefix", - action="store", type="string", dest="shorten_ids_prefix", default="", - help="shorten all ID attributes with a custom prefix") -_options_parser.add_option("--disable-embed-rasters", - action="store_false", dest="embed_rasters", default=True, - help="won't embed rasters as base64-encoded data") -_options_parser.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") -_options_parser.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") -_options_parser.add_option("--remove-metadata", - action="store_true", dest="remove_metadata", default=False, - help="remove elements (which may contain license metadata etc.)") -_options_parser.add_option("--renderer-workaround", - action="store_true", dest="renderer_workaround", default=True, - help="work around various renderer bugs (currently only librsvg) (default)") -_options_parser.add_option("--no-renderer-workaround", - action="store_false", dest="renderer_workaround", default=True, - help="do not work around various renderer bugs (currently only librsvg)") -_options_parser.add_option("--strip-xml-prolog", - action="store_true", dest="strip_xml_prolog", default=False, - help="won't output the prolog") -_options_parser.add_option("--enable-viewboxing", - action="store_true", dest="enable_viewboxing", default=False, - help="changes document width/height to 100%/100% and creates viewbox coordinates") - -# GZ: this is confusing, most people will be thinking in terms of -# decimal places, which is not what decimal precision is doing -_options_parser.add_option("-p", "--set-precision", - action="store", type=int, dest="digits", default=5, - help="set number of significant digits (default: %default)") -_options_parser.add_option("-i", - action="store", dest="infilename", help=optparse.SUPPRESS_HELP) -_options_parser.add_option("-o", - action="store", dest="outfilename", help=optparse.SUPPRESS_HELP) _options_parser.add_option("-q", "--quiet", 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 log output (eg report processed elements and such)") -_options_parser.add_option("--indent", - action="store", type="string", dest="indent_type", default="space", + 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") +_options_parser.add_option("-o", + 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)") +_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") +_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") +_option_group_optimization.add_option("--disable-group-collapsing", + 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") +_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") +_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") +_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)") +_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)") +_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 ()") +_option_group_document.add_option("--remove-metadata", + action="store_true", dest="remove_metadata", default=False, + help="remove elements (which may contain license/author information etc.)") +_option_group_document.add_option("--enable-comment-stripping", + 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") +_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") +_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)") -_options_parser.add_option("--nindent", - action="store", type=int, dest="indent_depth", default=1, +_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)") -_options_parser.add_option("--no-line-breaks", +_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\")") -_options_parser.add_option("--strip-xml-space", + 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") -_options_parser.add_option("--protect-ids-noninkscape", +_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") +_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") +_option_group_ids.add_option("--shorten-ids-prefix", + action="store", type="string", dest="shorten_ids_prefix", 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 change IDs not ending with a digit") -_options_parser.add_option("--protect-ids-list", - action="store", type="string", dest="protect_ids_list", default=None, - help="Don't change IDs given in a comma-separated list") -_options_parser.add_option("--protect-ids-prefix", - action="store", type="string", dest="protect_ids_prefix", default=None, - help="Don't change IDs starting with the given prefix") + 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") +_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") +_options_parser.add_option_group(_option_group_ids) From ea610e5c09940b3bf2d168f1669efab31877b686 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sat, 2 Apr 2016 01:54:27 +0200 Subject: [PATCH 049/270] Fix regression in 77906518c08b8c35848567c1ff9e53f0dda57dcb (accidentally removed default value for "shorten_ids_prefix") --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index b861bc9..3ff0adc 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3212,7 +3212,7 @@ _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") _option_group_ids.add_option("--shorten-ids-prefix", - action="store", type="string", dest="shorten_ids_prefix", metavar="PREFIX", + 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, From b14e801cb769553ead654667ef2759199e51cc1c Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 2 Apr 2016 16:49:10 +0200 Subject: [PATCH 050/270] add option to check and warn or bail out on flowtext --- Makefile | 6 +++ scour/scour.py | 12 ++++++ unittests/flowtext-less.svg | 66 +++++++++++++++++++++++++++++++ unittests/flowtext.svg | 78 +++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 unittests/flowtext-less.svg create mode 100644 unittests/flowtext.svg diff --git a/Makefile b/Makefile index bc870ee..e0365bb 100644 --- a/Makefile +++ b/Makefile @@ -17,3 +17,9 @@ test_version: test_help: PYTHONPATH=. python -m scour.scour --help + +test_error_on_flowtext: + # this is fine .. + 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 diff --git a/scour/scour.py b/scour/scour.py index 3ff0adc..0104a72 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2880,6 +2880,14 @@ def scourString(in_string, options=None): 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: + print("SVG input document uses {} flow text elements, which won't render on browsers!".format(cnt_flowText_el)) + if options.error_on_flowtext: + sys.exit(1) + # remove if the user wants to if options.remove_metadata: removeMetadataElements(doc) @@ -3225,6 +3233,10 @@ _option_group_ids.add_option("--protect-ids-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)") def maybe_gziped_file(filename, mode="r"): diff --git a/unittests/flowtext-less.svg b/unittests/flowtext-less.svg new file mode 100644 index 0000000..eea559c --- /dev/null +++ b/unittests/flowtext-less.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + abcd + + diff --git a/unittests/flowtext.svg b/unittests/flowtext.svg new file mode 100644 index 0000000..9409b4f --- /dev/null +++ b/unittests/flowtext.svg @@ -0,0 +1,78 @@ + + + + + + + + + + image/svg+xml + + + + + + + sfdadasdasdasdadsa abcd + + From 99dc0dfae9bf50082ce1124e50396fd2eedda452 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 2 Apr 2016 17:11:14 +0200 Subject: [PATCH 051/270] fix opt groups --- scour/scour.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scour/scour.py b/scour/scour.py index 0104a72..4d7995e 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3237,6 +3237,7 @@ _option_group_compatibility = optparse.OptionGroup(_options_parser, "SVG compati _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)") +_options_parser.add_option_group(_option_group_compatibility) def maybe_gziped_file(filename, mode="r"): From d710fb3f6c375455220e3286728683e9c2c53acc Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 2 Apr 2016 17:40:40 +0200 Subject: [PATCH 052/270] whitespace --- testscour.py | 103 ++++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/testscour.py b/testscour.py index 9697cfe..de8036f 100755 --- a/testscour.py +++ b/testscour.py @@ -34,9 +34,9 @@ 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 +# 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 +# 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): @@ -73,64 +73,64 @@ class InvalidOptions(unittest.TestCase): class NoInkscapeElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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' ) @@ -200,7 +200,7 @@ class KeepUnreferencedIDsWhenEnabled(unittest.TestCase): doc = scour.scourXmlFile('unittests/ids-to-strip.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', ' ID stripped when it should be disabled' ) - + class RemoveUnreferencedIDsWhenEnabled(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/ids-to-strip.svg', @@ -269,9 +269,9 @@ class NoInkscapeNamespaceDecl(unittest.TestCase): self.assertNotEqual(attrs.item(i).nodeValue, 'http://www.inkscape.org/namespaces/inkscape', 'Inkscape namespace declaration found' ) - + class NoSodipodiAttributes(unittest.TestCase): - def runTest(self): + def runTest(self): def findSodipodiAttr(elem): attrs = elem.attributes if attrs == None: return True @@ -279,12 +279,12 @@ 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, + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, findSodipodiAttr), False, 'Found Sodipodi attributes' ) - + class NoInkscapeAttributes(unittest.TestCase): - def runTest(self): + def runTest(self): def findInkscapeAttr(elem): attrs = elem.attributes if attrs == None: return True @@ -292,7 +292,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, + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement, findInkscapeAttr), False, 'Found Inkscape attributes' ) @@ -305,7 +305,7 @@ class KeepInkscapeNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): for i in range(len(attrs)): if attrs.item(i).nodeValue == 'http://www.inkscape.org/namespaces/inkscape': FoundNamespace = True - break + break self.assertEqual(True, FoundNamespace, "Did not find Inkscape namespace declaration when using --keep-editor-data") return False @@ -319,7 +319,7 @@ class KeepSodipodiNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): for i in range(len(attrs)): if attrs.item(i).nodeValue == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': FoundNamespace = True - break + break self.assertEqual(True, FoundNamespace, "Did not find Sodipodi namespace declaration when using --keep-editor-data") return False @@ -330,13 +330,13 @@ class KeepReferencedFonts(unittest.TestCase): fonts = doc.documentElement.getElementsByTagNameNS(SVGNS,'font') self.assertEqual(len(fonts), 1, 'Font wrongly removed from ' ) - + 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' ) - + class RemoveStrokeWhenStrokeTransparent(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/stroke-transparent.svg') @@ -414,7 +414,7 @@ class RemoveStrokeWhenStrokeNone(unittest.TestCase): doc = scour.scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', 'stroke attribute not emptied when no stroke' ) - + class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/stroke-none.svg') @@ -456,7 +456,7 @@ class RemoveFillRuleWhenFillNone(unittest.TestCase): 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') @@ -481,7 +481,7 @@ class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase): 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') @@ -491,7 +491,7 @@ class CollapseSinglyReferencedGradients(unittest.TestCase): class InheritGradientUnitsUponCollapsing(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/collapse-gradients.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), 'userSpaceOnUse', 'gradientUnits not properly inherited when collapsing gradients' ) @@ -525,9 +525,9 @@ 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], '-', + 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') @@ -552,7 +552,7 @@ class RoundPathData(unittest.TestCase): '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') @@ -629,7 +629,7 @@ class HandleSciNoInPathData(unittest.TestCase): 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] @@ -671,13 +671,13 @@ class AllowQuotEntitiesInUrl(unittest.TestCase): 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' ) - + class CollapseConsecutiveHLinesSegments(unittest.TestCase): def runTest(self): p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0] @@ -689,7 +689,7 @@ class CollapseConsecutiveHLinesCoords(unittest.TestCase): 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] @@ -699,9 +699,9 @@ class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase): 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', + 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] @@ -713,7 +713,7 @@ class DoNotRemovePolgonLastPoint(unittest.TestCase): 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] @@ -770,7 +770,7 @@ class RemoveDuplicateLinearGradients(unittest.TestCase): 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') @@ -779,14 +779,14 @@ class RereferenceForLinearGradient(unittest.TestCase): 'Rect not changed after removing duplicate linear gradient') self.assertEqual(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), 'Rect not changed 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') - + class RereferenceForRadialGradient(unittest.TestCase): def runTest(self): svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') @@ -912,7 +912,7 @@ class CDATAInXml(unittest.TestCase): def runTest(self): with open('unittests/cdata.svg') as f: lines = scour.scourString(f.read()).splitlines() - self.assertEqual( lines[3], + self.assertEqual( lines[3], " alert('pb&j');", 'CDATA did not come out correctly') @@ -990,7 +990,7 @@ class RemoveCommonAttributesFromChild(unittest.TestCase): 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') - + class DontRemoveCommonAttributesIfParentHasTextNodes(unittest.TestCase): def runTest(self): text = scour.scourXmlFile('unittests/move-common-attributes-to-parent.svg').getElementsByTagNameNS(SVGNS, 'text')[0] @@ -1002,7 +1002,7 @@ class PropagateCommonAttributesUp(unittest.TestCase): 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') - + class PathEllipticalArcParsingCommaWsp(unittest.TestCase): def runTest(self): p = scour.scourXmlFile('unittests/path-elliptical-arc-parsing.svg').getElementsByTagNameNS(SVGNS, 'path')[0] @@ -1050,9 +1050,9 @@ class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): '''.splitlines() for i in range(4): - self.assertEqual( s[i], c[i], + 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] @@ -1080,7 +1080,7 @@ 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, + self.assertEqual( len(s.splitlines()), 4, 'Did not output line ending character correctly') class XmlEntities(unittest.TestCase): @@ -1091,7 +1091,7 @@ class XmlEntities(unittest.TestCase): class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/comments.svg') - self.assertEqual( doc.childNodes.length, 4, + 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') @@ -1100,7 +1100,7 @@ class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): class DoNotStripDoctype(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/doctype.svg') - self.assertEqual( doc.childNodes.length, 3, + 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') @@ -1229,7 +1229,7 @@ class StyleToAttr(unittest.TestCase): 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') @@ -1416,6 +1416,7 @@ class DuplicateGradientsUpdateStyle(unittest.TestCase): self.assertEqual('fill:url(#' + gradientTag.getAttribute('id') + ')', rectTag1.getAttribute('style'), 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" was not updated to reflect this') + # TODO: write tests for --enable-viewboxing # TODO; write a test for embedding rasters # TODO: write a test for --disable-embed-rasters From 6a23a4cd714e9bf250684ee73ac7bc1da731d006 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 2 Apr 2016 17:41:20 +0200 Subject: [PATCH 053/270] add unit tests --- scour/scour.py | 6 ++++-- testscour.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 4d7995e..0112ad1 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2884,9 +2884,11 @@ 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: - print("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: - sys.exit(1) + raise Exception(errmsg) + else: + print("WARNING: {}".format(errmsg)) # remove if the user wants to if options.remove_metadata: diff --git a/testscour.py b/testscour.py index de8036f..af9ce96 100755 --- a/testscour.py +++ b/testscour.py @@ -1416,6 +1416,19 @@ class DuplicateGradientsUpdateStyle(unittest.TestCase): self.assertEqual('fill:url(#' + gradientTag.getAttribute('id') + ')', rectTag1.getAttribute('style'), 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" 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'])[0]) + +class DocWithNoFlowtext(unittest.TestCase): + def runTest(self): + try: + scour.scourXmlFile('unittests/flowtext-less.svg', + scour.parse_args(['--error-on-flowtext'])[0]) + except Exception as e: + self.fail("exception '{}' was raised, and we didn't expect that!".format(e)) # TODO: write tests for --enable-viewboxing # TODO; write a test for embedding rasters From 6bb2b35ba2a0d7cfec61a9ebe1a9f831e8e8fb13 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sat, 2 Apr 2016 19:30:30 +0200 Subject: [PATCH 054/270] Continuous integration with Travis and tox --- .travis.yml | 24 ++++++++++++++++++++++++ tox.ini | 4 ++++ 2 files changed, 28 insertions(+) create mode 100644 .travis.yml create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..890c175 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: python + +install: + - pip install tox + +env: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=py33 + - TOX_ENV=py34 +# - TOX_ENV=py35 + - TOX_ENV=pypy + +script: + - tox -e $TOX_ENV + +matrix: + fast_finish: true + + include: + # https://github.com/travis-ci/travis-ci/issues/4794#issuecomment-143758799 + - python: 3.5 + env: + - TOX_ENV=py35 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2939feb --- /dev/null +++ b/tox.ini @@ -0,0 +1,4 @@ +[testenv] +commands = + scour --version + python testscour.py From 84b36c710987a34dd42cb690ab0f0b78b8c5c27a Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 12 Jun 2016 15:36:02 +0200 Subject: [PATCH 055/270] Only include "standalone" attribute if it was explicitly set to "yes" in input document ("no" is the default value) --- scour/scour.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 0112ad1..1f14181 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3090,7 +3090,10 @@ def scourString(in_string, options=None): # return the string with its XML prolog and surrounding comments if options.strip_xml_prolog == False: - total_output = '\n' + total_output = '\n' else: total_output = "" From f83b03707b62197346589fa065c1fcb822171ba1 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 12 Jun 2016 15:42:30 +0200 Subject: [PATCH 056/270] Fix unittests for previous commit --- testscour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testscour.py b/testscour.py index af9ce96..ee2eef7 100755 --- a/testscour.py +++ b/testscour.py @@ -1031,7 +1031,7 @@ class DoNotPrettyPrintWhenWhitespacePreserved(unittest.TestCase): def runTest(self): with open('unittests/whitespace-important.svg') as f: s = scour.scourString(f.read()).splitlines() - c = ''' + c = ''' This is some messed-up markup @@ -1044,7 +1044,7 @@ class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): def runTest(self): with open('unittests/whitespace-nested.svg') as f: s = scour.scourString(f.read()).splitlines() - c = ''' + c = ''' Use bold text From 371f14786a4eb11305004b9d864a6f4f650846d1 Mon Sep 17 00:00:00 2001 From: Dirk Thomas Date: Fri, 29 Jul 2016 10:22:59 -0700 Subject: [PATCH 057/270] add --order-attributes option --- scour/scour.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 1f14181..181d604 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2792,7 +2792,11 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): # now serialize the other attributes attrList = element.attributes - for num in range(attrList.length) : + attrIndices = range(attrList.length) + if options.order_attributes : + attrName2Index = dict([(attrList.item(i).nodeName, i) for i in attrIndices]) + attrIndices = [attrName2Index[name] for name in sorted(attrName2Index.keys())] + for num in attrIndices : attr = attrList.item(num) if attr.nodeName == 'id' or attr.nodeName == 'xml:id': continue # if the attribute value contains a double-quote, use single-quotes @@ -3215,6 +3219,9 @@ _option_group_formatting.add_option("--no-line-breaks", _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") +_option_group_formatting.add_option("--order-attributes", + action="store_true", dest="order_attributes", default=False, + help="order attributes alphabetically (except \"id\")") _options_parser.add_option_group(_option_group_formatting) _option_group_ids = optparse.OptionGroup(_options_parser, "ID attributes") From a766a3256bc40db31d05f59b9125961138251cf1 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 30 Jul 2016 13:14:43 +0200 Subject: [PATCH 058/270] force travis --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4d4c237..194a380 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Scour +**Build status:** [![Build Status](https://travis-ci.org/scour-project/scour.svg?branch=master](https://travis-ci.org/scour-project/scour) + +--- + Scour is a Python tool that takes an SVG file and produces a cleaner and more concise file. It is intended to be used **after** exporting to SVG with a GUI editor, such as Inkscape or Adobe Illustrator. Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). From e41893d98fd64c19faa406697d10d50b5d42dd3e Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 30 Jul 2016 15:17:14 +0200 Subject: [PATCH 059/270] trigger build --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 194a380..2c42a3b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Scour -**Build status:** [![Build Status](https://travis-ci.org/scour-project/scour.svg?branch=master](https://travis-ci.org/scour-project/scour) +**Build status:** [![Build Status](https://travis-ci.org/scour-project/scour.svg?branch=master]](https://travis-ci.org/scour-project/scour) --- From 06457a7461259775c254bd92807ba1059e7da197 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sat, 30 Jul 2016 15:21:17 +0200 Subject: [PATCH 060/270] trigger build --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c42a3b..d4452f1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Scour -**Build status:** [![Build Status](https://travis-ci.org/scour-project/scour.svg?branch=master]](https://travis-ci.org/scour-project/scour) +**Build status:** [![Build Status](https://travis-ci.org/scour-project/scour.svg?branch=master)](https://travis-ci.org/scour-project/scour) --- From 3f5c6c76c07bf2a93fc87f1dfa58e75efcfba918 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sat, 13 Aug 2016 17:14:29 +0200 Subject: [PATCH 061/270] Sync version --- scour/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/__init__.py b/scour/__init__.py index 30d73cf..8ba6dd7 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ ## ############################################################################### -__version__ = u'0.33' +__version__ = u'0.34' From df142a2f2248d4dc997d4c19aacf1656f8e42d10 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sat, 13 Aug 2016 17:15:03 +0200 Subject: [PATCH 062/270] Drop official support for Python 2.6 --- .travis.yml | 2 +- README.md | 2 +- scour/scour.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 890c175..e423675 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ install: - pip install tox env: - - TOX_ENV=py26 +# - TOX_ENV=py26 - TOX_ENV=py27 - TOX_ENV=py33 - TOX_ENV=py34 diff --git a/README.md b/README.md index d4452f1..ceab3f6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This Github repository is the official one. The official website as well as olde ## Installation -Scour requires [Python](https://www.python.org) 2.6, 2.7 or 3.3+. Further, for installation, [pip](https://pip.pypa.io) should be used. +Scour requires [Python](https://www.python.org) 2.7 or 3.3+. Further, for installation, [pip](https://pip.pypa.io) should be used. To install the [latest release](https://pypi.python.org/pypi/scour) of Scour from PyPI: diff --git a/scour/scour.py b/scour/scour.py index 181d604..b74cf6a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -68,7 +68,7 @@ from six.moves import range try: from decimal import Decimal, InvalidOperation, getcontext except ImportError: - print("Scour requires at least Python 2.6+ or Python 3.3+.") + print("Scour requires at least Python 2.7 or Python 3.3+.") # Import Psyco if available try: From 3299f8f6e065e9fd1c01f1a81704e8883536b7b5 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 14 Aug 2016 17:12:48 +0200 Subject: [PATCH 063/270] Also shorten unused IDs when `--shorten-ids` is specified --- scour/scour.py | 98 ++++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index b74cf6a..f65e271 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -658,10 +658,13 @@ def shortenIDs(doc, prefix, unprotectedElements=None): # 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 + 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]) curIdNum = 1 @@ -712,59 +715,62 @@ def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): definingNode.setAttribute("id", idTo) del identifiedElements[idFrom] identifiedElements[idTo] = definingNode + num += len(idFrom) - len(idTo) - referringNodes = referencedIDs[idFrom] + # 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) - - # now try the fill, stroke, filter attributes - for attr in referencingProps: - oldValue = node.getAttribute(attr) - if oldValue != '': - newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + 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(attr, newValue) - num += len(oldValue) - len(newValue) + node.setAttribute('style', newValue) + num += len(styles) - len(newValue) - del referencedIDs[idFrom] - referencedIDs[idTo] = referringNodes + # 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 return num From fe2884c3e8f4eecc552af49dc7bb0569f4abeba2 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Sun, 14 Aug 2016 18:52:55 +0200 Subject: [PATCH 064/270] Don't remove unreferenced defs if `--keep-unreferenced-defs` is specified (#62) * Don't remove unreferenced defs if `--keep-unreferenced-defs` is specified (fixes #18) * Add unittests for previous commit --- scour/scour.py | 4 +++- testscour.py | 29 +++++++++++++++++++++++++++++ unittests/unreferenced-defs.svg | 19 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 unittests/unreferenced-defs.svg diff --git a/scour/scour.py b/scour/scour.py index f65e271..5e9aaf0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -623,7 +623,9 @@ def removeUnreferencedElements(doc, keepDefs): for id in identifiedElements: if not id in referencedIDs: goner = identifiedElements[id] - if goner != None and goner.parentNode != None and goner.nodeName in removeTags: + 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 diff --git a/testscour.py b/testscour.py index ee2eef7..06eaf49 100755 --- a/testscour.py +++ b/testscour.py @@ -176,6 +176,35 @@ class RemoveUnreferencedElementInDefs(unittest.TestCase): 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' ) + +class KeepUnreferencedDefs(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/unreferenced-defs.svg', + scour.parse_args(['--keep-unreferenced-defs'])[0]) + 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') diff --git a/unittests/unreferenced-defs.svg b/unittests/unreferenced-defs.svg new file mode 100644 index 0000000..2fd8a26 --- /dev/null +++ b/unittests/unreferenced-defs.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + From 57f93efc894b1ba4c30e42b549b0081185029c36 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 16 Aug 2016 00:10:41 +0200 Subject: [PATCH 065/270] Don't escape quotes ('/") in text nodes and attributes. (#64) - In text nodes quotes are fine - In attributes quotes are fine if used reciprocally. Escaping in the latter case often causes issues, e.g. with quoted font names (#21) or inline CSS styles (#56), while it probably does not gain anything (if quotes are wrongly used in attribute names the XML is most likely invalid to start with) --- scour/scour.py | 5 ++++- testscour.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 5e9aaf0..430cfa1 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2752,7 +2752,10 @@ def remapNamespacePrefix(node, oldprefix, newprefix): def makeWellFormed(str): - 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: diff --git a/testscour.py b/testscour.py index 06eaf49..322b7f9 100755 --- a/testscour.py +++ b/testscour.py @@ -1114,7 +1114,7 @@ class EnsureLineEndings(unittest.TestCase): class XmlEntities(unittest.TestCase): def runTest(self): - self.assertEqual( scour.makeWellFormed('<>&"\''), '<>&"'', + self.assertEqual( scour.makeWellFormed('<>&'), '<>&', 'Incorrectly translated XML entities') class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): From 4f23ea7a342ef9e191a27d99db7cbb1063d975a3 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 23 Aug 2016 21:16:14 +0200 Subject: [PATCH 066/270] Print usage information if no input file was specified (and no data is available from `stdin`) (#65) fixes #34 --- scour/scour.py | 79 ++++++++++++++++++++++++++++---------------------- testscour.py | 32 ++++++++++---------- 2 files changed, 61 insertions(+), 50 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 430cfa1..e7ac104 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3263,13 +3263,6 @@ _option_group_compatibility.add_option("--error-on-flowtext", _options_parser.add_option_group(_option_group_compatibility) -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) - - def parse_args(args=None, ignore_additional_args=False): options, rargs = _options_parser.parse_args(args) @@ -3290,6 +3283,42 @@ def parse_args(args=None, ignore_additional_args=False): 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) + + 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('__')) + + 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) + + + +def getInOut(options): if options.infilename: infile = maybe_gziped_file(options.infilename, "rb") # GZ: could catch a raised IOError here and report @@ -3298,9 +3327,13 @@ def parse_args(args=None, ignore_additional_args=False): # # open the binary buffer of stdin and let XML parser handle decoding try: - infile = sys.stdin.buffer + infile = sys.stdin.buffer except AttributeError: - infile = sys.stdin + 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: @@ -3310,7 +3343,7 @@ def parse_args(args=None, ignore_additional_args=False): except AttributeError: outfile = sys.stdout - return options, [infile, outfile] + return [infile, outfile] @@ -3331,29 +3364,6 @@ def getReport(): -def generateDefaultOptions(): - ## FIXME: clean up this mess/hack and refactor arg parsing to argparse - class Struct: - def __init__(self, **entries): - self.__dict__.update(entries) - - d = parse_args(args = [], ignore_additional_args = True)[0].__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('__')) - - sanitizedOptions = _options_parser.get_default_values() - sanitizedOptions._update_careful(optionsDict) - - return sanitizedOptions - - - def start(options, input, output): start = walltime() @@ -3388,7 +3398,8 @@ def start(options, input, output): def run(): - options, (input, output) = parse_args() + options = parse_args() + (input, output) = getInOut(options) start(options, input, output) diff --git a/testscour.py b/testscour.py index 322b7f9..9c29e91 100755 --- a/testscour.py +++ b/testscour.py @@ -193,7 +193,7 @@ class RemoveUnreferencedDefs(unittest.TestCase): class KeepUnreferencedDefs(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/unreferenced-defs.svg', - scour.parse_args(['--keep-unreferenced-defs'])[0]) + 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, @@ -233,7 +233,7 @@ 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'])[0]) + scour.parse_args(['--enable-id-stripping'])) self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), '', ' ID not stripped' ) @@ -246,7 +246,7 @@ 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'])[0]) + scour.parse_args(['--disable-group-collapsing'])) self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, 'Useless nested groups were removed despite --disable-group-collapsing' ) @@ -495,7 +495,7 @@ class RemoveFillOpacityWhenFillNone(unittest.TestCase): class ConvertFillPropertyToAttr(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/fill-none.svg', - scour.parse_args(['--disable-simplify-colors'])[0]) + scour.parse_args(['--disable-simplify-colors'])) self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill'), 'black', 'fill property not converted to XML attribute' ) @@ -1144,7 +1144,7 @@ class PathImplicitLineWithMoveCommands(unittest.TestCase): class RemoveMetadataOption(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/full-metadata.svg', - scour.parse_args(['--remove-metadata'])[0]) + scour.parse_args(['--remove-metadata'])) self.assertEqual(doc.childNodes.length, 1, 'Did not remove tag with --remove-metadata') @@ -1153,7 +1153,7 @@ class EnableCommentStrippingOption(unittest.TestCase): with open('unittests/comment-beside-xml-decl.svg') as f: docStr = f.read() docStr = scour.scourString(docStr, - scour.parse_args(['--enable-comment-stripping'])[0]) + scour.parse_args(['--enable-comment-stripping'])) self.assertEqual(docStr.find(')") diff --git a/testscour.py b/testscour.py index 52f243c..ba0ceba 100755 --- a/testscour.py +++ b/testscour.py @@ -141,15 +141,59 @@ class NoAdobeXPathElements(unittest.TestCase): 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' ) + +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' ) + +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' ) + +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' ) + class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/metadata-with-text.svg') + 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-metadata.svg') + 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' ) + +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' ) @@ -1152,13 +1196,34 @@ class PathImplicitLineWithMoveCommands(unittest.TestCase): 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') + +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 tag with --remove-descriptions') + class RemoveMetadataOption(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/full-metadata.svg', + doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', scour.parse_args(['--remove-metadata'])) self.assertEqual(doc.childNodes.length, 1, 'Did not remove 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 , <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: diff --git a/unittests/metadata-with-text.svg b/unittests/descriptive-elements-with-text.svg similarity index 57% rename from unittests/metadata-with-text.svg rename to unittests/descriptive-elements-with-text.svg index 6149b68..c991ddd 100644 --- a/unittests/metadata-with-text.svg +++ b/unittests/descriptive-elements-with-text.svg @@ -1,4 +1,6 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg"> + <title>This is a title element with only text node children + This is a desc element with only text node children This is a metadata element with only text node children diff --git a/unittests/empty-metadata.svg b/unittests/empty-descriptive-elements.svg similarity index 67% rename from unittests/empty-metadata.svg rename to unittests/empty-descriptive-elements.svg index ca3c31f..2790084 100644 --- a/unittests/empty-metadata.svg +++ b/unittests/empty-descriptive-elements.svg @@ -1,3 +1,5 @@ + + diff --git a/unittests/full-metadata.svg b/unittests/full-descriptive-elements.svg similarity index 62% rename from unittests/full-metadata.svg rename to unittests/full-descriptive-elements.svg index f67e01d..8decf2d 100644 --- a/unittests/full-metadata.svg +++ b/unittests/full-descriptive-elements.svg @@ -1,4 +1,13 @@ + + This is an example SVG file + Unit test for Scour's --remove-titles option + + + This is an example SVG file + Unit test for Scour's + --remove-descriptions option + Date: Tue, 30 Aug 2016 00:05:52 +0200 Subject: [PATCH 084/270] Fix removal rules for the `overflow` attribute (#104) (fixes #92) --- scour/scour.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index d963b78..4acdcab 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1529,22 +1529,22 @@ def repairStyle(node, options): num += 1 if 'overflow' in styleMap : - # overflow specified on element other than svg, marker, pattern - if not node.nodeName in ['svg','marker','pattern']: + # 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 - # it is a marker, pattern or svg - # as long as this node is not the document , then only - # remove overflow='hidden'. See - # http://www.w3.org/TR/2010/WD-SVG11-20100622/masking.html#OverflowProperty + # 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 - # else if outer svg has a overflow="visible", we can remove it + # 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 + 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 From fa17e6655a8f7041326036d22181bc977e3177fe Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Tue, 30 Aug 2016 00:13:37 +0200 Subject: [PATCH 085/270] Update HISTORY.md --- HISTORY.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 73da90f..4315efd 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,6 +13,11 @@ * Improve code to remove default attribute values and add a lot of new default values. ([#70](https://github.com/scour-project/scour/issues/70)) * Fix: Only attempt to group elements that the content model allows to be children of a `` when `--create-groups` is specified. (#98) * Fix: Update list of SVG presentation attributes allowing more styles to be converted to attributes and remove two entries (`line-height` and `visibility`) that were actually invalid. (#99) +* Add three options that work analoguous to `--remove-metadata` (removes elements) (#102) + * `--remove-titles` (removes elements) + * `--remove-descriptions` (removes elements) + * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. , <desc> and <metadata>) +* Fix removal rules for the `overflow` attribute (#104) ## Version 0.34 (2016-07-25) From e57adf0c96e72230cc5c639615f2c12092b9aab2 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Tue, 30 Aug 2016 00:15:09 +0200 Subject: [PATCH 086/270] Fix HISTORY.md --- HISTORY.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 4315efd..096c8d8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,10 +13,10 @@ * Improve code to remove default attribute values and add a lot of new default values. ([#70](https://github.com/scour-project/scour/issues/70)) * Fix: Only attempt to group elements that the content model allows to be children of a `<g>` when `--create-groups` is specified. (#98) * Fix: Update list of SVG presentation attributes allowing more styles to be converted to attributes and remove two entries (`line-height` and `visibility`) that were actually invalid. (#99) -* Add three options that work analoguous to `--remove-metadata` (removes <metadata> elements) (#102) - * `--remove-titles` (removes <title> elements) - * `--remove-descriptions` (removes <desc> elements) - * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. <title>, <desc> and <metadata>) +* Add three options that work analoguous to `--remove-metadata` (removes `<metadata>` elements) (#102) + * `--remove-titles` (removes `<title>` elements) + * `--remove-descriptions` (removes `<desc>` elements) + * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. `<title>`, `<desc>` and `<metadata>`) * Fix removal rules for the `overflow` attribute (#104) From bf4fc1b70ec7149e4e32ec276442475906593aa5 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Tue, 30 Aug 2016 00:17:33 +0200 Subject: [PATCH 087/270] Linkify HISTORY.md --- HISTORY.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 096c8d8..7f5ae6b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,13 +11,13 @@ * Redirect informational output to `stderr` when SVG is output to `stdout`. ([#67](https://github.com/scour-project/scour/issues/67)) * Allow elements to be found via `Document.getElementById()` in the minidom document returned by scourXmlFile(). ([#68](https://github.com/scour-project/scour/issues/68)) * Improve code to remove default attribute values and add a lot of new default values. ([#70](https://github.com/scour-project/scour/issues/70)) -* Fix: Only attempt to group elements that the content model allows to be children of a `<g>` when `--create-groups` is specified. (#98) -* Fix: Update list of SVG presentation attributes allowing more styles to be converted to attributes and remove two entries (`line-height` and `visibility`) that were actually invalid. (#99) -* Add three options that work analoguous to `--remove-metadata` (removes `<metadata>` elements) (#102) +* Fix: Only attempt to group elements that the content model allows to be children of a `<g>` when `--create-groups` is specified. ([#98](https://github.com/scour-project/scour/issues/98)) +* Fix: Update list of SVG presentation attributes allowing more styles to be converted to attributes and remove two entries (`line-height` and `visibility`) that were actually invalid. ([#99](https://github.com/scour-project/scour/issues/99)) +* Add three options that work analoguous to `--remove-metadata` (removes `<metadata>` elements) ([#102](https://github.com/scour-project/scour/issues/102)) * `--remove-titles` (removes `<title>` elements) * `--remove-descriptions` (removes `<desc>` elements) * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. `<title>`, `<desc>` and `<metadata>`) -* Fix removal rules for the `overflow` attribute (#104) +* Fix removal rules for the `overflow` attribute ([#104](https://github.com/scour-project/scour/issues/104)) ## Version 0.34 (2016-07-25) From e76da093ea83f5a80c4624cd3514bdc043aec7ce Mon Sep 17 00:00:00 2001 From: Dirk Thomas <dthomas@osrfoundation.org> Date: Tue, 30 Aug 2016 13:30:29 -0700 Subject: [PATCH 088/270] Try to always order attributes intuitively based on a list of select attributes, order other attributes alphabetical (#105) --- scour/scour.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 4acdcab..d4e55bc 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2970,11 +2970,24 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): outParts.extend([' xml:id=', quot, id, quot]) # now serialize the other attributes + known_attr = [ + 'id', 'class', + 'transform', + 'x', 'y', 'z', 'width', 'height', 'x1', 'x2', 'y1', 'y2', + 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', + 'd', + ] + sorted(svgAttributes) + [ + 'style', + ] attrList = element.attributes - attrIndices = range(attrList.length) - if options.order_attributes : - attrName2Index = dict([(attrList.item(i).nodeName, i) for i in attrIndices]) - attrIndices = [attrName2Index[name] for name in sorted(attrName2Index.keys())] + 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 @@ -3419,9 +3432,6 @@ _option_group_formatting.add_option("--no-line-breaks", _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") -_option_group_formatting.add_option("--order-attributes", - action="store_true", dest="order_attributes", default=False, - help="order attributes alphabetically (except \"id\")") _options_parser.add_option_group(_option_group_formatting) _option_group_ids = optparse.OptionGroup(_options_parser, "ID attributes") From 3929426a5aabbb6819b0d9eb473492e1aa075a7d Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Tue, 30 Aug 2016 22:32:57 +0200 Subject: [PATCH 089/270] Add some attributes that fit into the list of the previous commit, too --- scour/scour.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index d4e55bc..31cf1e7 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2971,11 +2971,14 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): # 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', + 'd', 'points', ] + sorted(svgAttributes) + [ 'style', ] From 9e4b9d6f5ed680d8c269c97641fae5ad68c5c9c2 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Wed, 31 Aug 2016 00:04:19 +0200 Subject: [PATCH 090/270] Improve behaviour of numerial precision reduction * Previously all calculations were immediately done with the precision specified by the user, resulting in accumulation of numerical errors and in some cases very discernible abberations from the initial image (e.g. #80) * Now all calculations are done with default precision (the module "decimal" uses a whopping 28 signifigant digits initially!) and only at the very end coordinates are rounded to the desired precision. --- scour/scour.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 31cf1e7..17168ef 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -64,12 +64,7 @@ import optparse from scour.yocto_css import parseCssString import six from six.moves import range - -# Python 2.3- did not have Decimal -try: - from decimal import Decimal, InvalidOperation, getcontext -except ImportError: - sys.stderr.write("Scour requires at least Python 2.7 or Python 3.3+.") +from decimal import Context, Decimal, InvalidOperation, getcontext # select the most precise walltime measurement function available on the platform if sys.platform.startswith('win'): @@ -2474,9 +2469,13 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a This is faster than scourLength on elements guaranteed not to contain units. """ - # reduce to the proper number of digits 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) + # if the value is an integer, it may still have .0[...] attached to it for some reason # remove those if int(length) == length: @@ -3068,7 +3067,11 @@ def scourString(in_string, options=None): # sanitize options (take missing attributes from defaults, discard unknown attributes) options = sanitizeOptions(options) - getcontext().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 From 29e005bf7b7212fb68b9058879c73a3e88bdb07f Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Wed, 31 Aug 2016 06:29:01 +0200 Subject: [PATCH 091/270] Improve handling of scientific vs. non-scientific notation * Negative exponents were not handled in a reasonable way (e.g. 0.000001 remained unchanged) * Trailing zeroes were not always properly removed --- scour/scour.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 17168ef..bd0ced5 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2476,10 +2476,12 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a # plus() corresponds to the unary prefix plus operator and applies context precision and rounding length = scouringContext.plus(length) - # if the value is an integer, it may still have .0[...] attached to it for some reason - # remove those - if int(length) == length: - length = getcontext().create_decimal(int(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() # gather the non-scientific notation version of the coordinate. # this may actually be in scientific notation if the value is @@ -2491,14 +2493,22 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a elif len(nonsci) > 3 and nonsci[:3] == '-0.': nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot - if len(nonsci) > 3: # avoid calling normalize unless strictly necessary - # and then the scientific notation version, with E+NUMBER replaced with - # just eNUMBER, since SVG accepts this. - sci = six.text_type(length.normalize()).lower().replace("e+", "e") + # 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 - 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 From 21e6c7491b158e01327ae2d368a0cfa7f23a1740 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Wed, 31 Aug 2016 06:32:05 +0200 Subject: [PATCH 092/270] Fix a unittest that failed due to the increased accuracy of paths after 29e005bf7b7212fb68b9058879c73a3e88bdb07f --- testscour.py | 2 +- unittests/collapse-same-path-points.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testscour.py b/testscour.py index ba0ceba..ad02ec3 100755 --- a/testscour.py +++ b/testscour.py @@ -877,7 +877,7 @@ class RereferenceForRadialGradient(unittest.TestCase): class CollapseSamePathPoints(unittest.TestCase): def runTest(self): p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0]; - self.assertEqual(p.getAttribute('d'), "m100 100l100.12 100.12c14.88 4.88-15.12-5.12 0 0z", + 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): diff --git a/unittests/collapse-same-path-points.svg b/unittests/collapse-same-path-points.svg index bda0fff..b05f4d1 100644 --- a/unittests/collapse-same-path-points.svg +++ b/unittests/collapse-same-path-points.svg @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="210" height="210"> - <path stroke="yellow" fill="red" d="M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"/> + <path stroke="yellow" fill="red" d="M100,100 L200.12345,200.12345 C215,205 185,195 200.12345,200.12345 Z"/> </svg> From 0c63344ea431a1f677b3eb66bacff1fc63689563 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Wed, 31 Aug 2016 07:06:42 +0200 Subject: [PATCH 093/270] Add a check to prevent we make path data longer by "optimization" --- scour/scour.py | 8 ++++++-- testscour.py | 8 ++++++++ unittests/path-no-optimize.svg | 4 ++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 unittests/path-no-optimize.svg diff --git a/scour/scour.py b/scour/scour.py index bd0ced5..4af6384 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2286,8 +2286,12 @@ def cleanPath(element, options) : path = newPath newPathStr = serializePath(path, options) - numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) - element.setAttribute('d', newPathStr) + + # 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) diff --git a/testscour.py b/testscour.py index ad02ec3..f409225 100755 --- a/testscour.py +++ b/testscour.py @@ -685,6 +685,14 @@ class ChangeQuadToShorthandInPath(unittest.TestCase): 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" + class HandleEncodingUTF8(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/encoding-utf8.svg') diff --git a/unittests/path-no-optimize.svg b/unittests/path-no-optimize.svg new file mode 100644 index 0000000..bda0fff --- /dev/null +++ b/unittests/path-no-optimize.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="210" height="210"> + <path stroke="yellow" fill="red" d="M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"/> +</svg> From dc3f66ed0f37ab967ae646dd136f67d8c1f16f91 Mon Sep 17 00:00:00 2001 From: Dirk Thomas <dthomas@osrfoundation.org> Date: Wed, 31 Aug 2016 13:29:01 -0700 Subject: [PATCH 094/270] Sort declarations in `style` attribute (#107) --- scour/scour.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index 31cf1e7..4a2c1b3 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3000,6 +3000,9 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): quot = "'" 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 From 564367f886ff16f01cba5c45ef5c1e8bc9ff2747 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Wed, 31 Aug 2016 22:46:48 +0200 Subject: [PATCH 095/270] Update HISTORY.md --- HISTORY.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 7f5ae6b..ea1577f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,7 +2,6 @@ ## Version 0.35 (not released yet) -* Add `--order-attributes` option which orders attributes alphabetically in output. ([#59](https://github.com/scour-project/scour/issues/59)) * Drop official support for Python 2.6. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 2.6 and should find/fix any compatibility issues pull requests are welcome, though.) * Fix: Unused IDs were not shortended when `--shorten-ids` was used. * Fix: Most elements were still removed from `<defs>` when `--keep-unreferenced-defs` was used. ([#62](https://github.com/scour-project/scour/issues/62)) @@ -18,6 +17,8 @@ * `--remove-descriptions` (removes `<desc>` elements) * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. `<title>`, `<desc>` and `<metadata>`) * Fix removal rules for the `overflow` attribute ([#104](https://github.com/scour-project/scour/issues/104)) +* Improvement: Automatically order all attributes ([#105](https://github.com/scour-project/scour/issues/105)), as well as `style` declarations ([#107](https://github.com/scour-project/scour/issues/107)) allowing for a constant output across multiple runs of Scour. Before order could change arbitrarily. +* Improve path scouring. ([#108](https://github.com/scour-project/scour/issues/108))<br>Notably Scour performs all caculations with enhanced precision now, guaranteeing maximum accuracy when optimizing path data. Numerical precision is reduced as a last step of the optimization according to the `--precision` option. ## Version 0.34 (2016-07-25) From ec855211de8bf1929b471ada6fd7e2d03228fbfc Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Mon, 5 Sep 2016 22:44:55 +0200 Subject: [PATCH 096/270] Fix replacement of duplicate gradients if "fill/stroke" contains fallbacks (#109) (fixes #79) --- scour/scour.py | 17 ++++++++----- testscour.py | 25 +++++++++++++------ .../duplicate-gradients-update-style.svg | 1 + unittests/remove-duplicate-gradients.svg | 1 + 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index afee25d..754e5dd 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1381,22 +1381,27 @@ def removeDuplicateGradients(doc): 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 + 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') - # for each element that referenced the gradient we are going to remove + 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) - if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": - elem.setAttribute(attr, 'url(#'+master_id+')') + (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] - if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": - styles[style] = 'url(#'+master_id+')' + (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 diff --git a/testscour.py b/testscour.py index f409225..294cb7f 100755 --- a/testscour.py +++ b/testscour.py @@ -864,9 +864,9 @@ class RereferenceForLinearGradient(unittest.TestCase): svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') self.assertEqual(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), - 'Rect not changed after removing duplicate linear gradient') + 'Reference not updated after removing duplicate linear gradient') self.assertEqual(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), - 'Rect not changed after removing duplicate linear gradient') + 'Reference not updated after removing duplicate linear gradient') class RemoveDuplicateRadialGradients(unittest.TestCase): def runTest(self): @@ -880,7 +880,15 @@ class RereferenceForRadialGradient(unittest.TestCase): svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') self.assertEqual(rects[2].getAttribute('stroke'), rects[3].getAttribute('fill'), - 'Rect not changed after removing duplicate radial gradient') + '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') + class CollapseSamePathPoints(unittest.TestCase): def runTest(self): @@ -1536,13 +1544,14 @@ class DuplicateGradientsUpdateStyle(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/duplicate-gradients-update-style.svg', scour.parse_args(['--disable-style-to-xml'])) - gradientTag = doc.getElementsByTagName('linearGradient')[0] - rectTag0 = doc.getElementsByTagName('rect')[0] - rectTag1 = doc.getElementsByTagName('rect')[1] - self.assertEqual('fill:url(#' + gradientTag.getAttribute('id') + ')', rectTag0.getAttribute('style'), + 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(#' + gradientTag.getAttribute('id') + ')', rectTag1.getAttribute('style'), + 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): diff --git a/unittests/duplicate-gradients-update-style.svg b/unittests/duplicate-gradients-update-style.svg index c28070c..b18d7b9 100644 --- a/unittests/duplicate-gradients-update-style.svg +++ b/unittests/duplicate-gradients-update-style.svg @@ -12,4 +12,5 @@ </defs> <rect style="fill: url(#duplicate-one)" width="200" height="200"/> <rect style="fill: url(#duplicate-two)" width="200" height="200" y="200"/> +<rect style="fill: url(#duplicate-two) #fff" width="200" height="200" y="200"/> </svg> \ No newline at end of file diff --git a/unittests/remove-duplicate-gradients.svg b/unittests/remove-duplicate-gradients.svg index f986bdd..d84c089 100644 --- a/unittests/remove-duplicate-gradients.svg +++ b/unittests/remove-duplicate-gradients.svg @@ -20,4 +20,5 @@ <rect id="r3" stroke="url(#g3)" width="100" height="100"/> <rect id="r4" fill='url("#g4")' width="100" height="100"/> <rect id="r5" fill="url(#g5)" width="100" height="100"/> + <rect id="r6" fill="url(#g5) #fff" width="100" height="100"/> </svg> From 0fac95ee0950db79ab85065536e72eeca59e993a Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Mon, 5 Sep 2016 22:46:11 +0200 Subject: [PATCH 097/270] Update HISTORY.md --- HISTORY.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index ea1577f..a15e345 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,10 +3,10 @@ ## Version 0.35 (not released yet) * Drop official support for Python 2.6. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 2.6 and should find/fix any compatibility issues pull requests are welcome, though.) -* Fix: Unused IDs were not shortended when `--shorten-ids` was used. +* Fix: Unused IDs were not shortended when `--shorten-ids` was used. ([#19](https://github.com/scour-project/scour/issues/62)) * Fix: Most elements were still removed from `<defs>` when `--keep-unreferenced-defs` was used. ([#62](https://github.com/scour-project/scour/issues/62)) * Improve escaping of single/double quotes ('/") in attributes. ([#64](https://github.com/scour-project/scour/issues/64)) -* Print usage information if no input file was specified (and no data is available from `stdin`).([#65](https://github.com/scour-project/scour/issues/65)) +* Print usage information if no input file was specified (and no data is available from `stdin`). ([#65](https://github.com/scour-project/scour/issues/65)) * Redirect informational output to `stderr` when SVG is output to `stdout`. ([#67](https://github.com/scour-project/scour/issues/67)) * Allow elements to be found via `Document.getElementById()` in the minidom document returned by scourXmlFile(). ([#68](https://github.com/scour-project/scour/issues/68)) * Improve code to remove default attribute values and add a lot of new default values. ([#70](https://github.com/scour-project/scour/issues/70)) @@ -16,9 +16,10 @@ * `--remove-titles` (removes `<title>` elements) * `--remove-descriptions` (removes `<desc>` elements) * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. `<title>`, `<desc>` and `<metadata>`) -* Fix removal rules for the `overflow` attribute ([#104](https://github.com/scour-project/scour/issues/104)) +* Fix removal rules for the `overflow` attribute. ([#104](https://github.com/scour-project/scour/issues/104)) * Improvement: Automatically order all attributes ([#105](https://github.com/scour-project/scour/issues/105)), as well as `style` declarations ([#107](https://github.com/scour-project/scour/issues/107)) allowing for a constant output across multiple runs of Scour. Before order could change arbitrarily. * Improve path scouring. ([#108](https://github.com/scour-project/scour/issues/108))<br>Notably Scour performs all caculations with enhanced precision now, guaranteeing maximum accuracy when optimizing path data. Numerical precision is reduced as a last step of the optimization according to the `--precision` option. +* Fix replacement of removed duplicate gradients if the `fill`/`stroke` properties contained a fallback. ([#109](https://github.com/scour-project/scour/issues/109)) ## Version 0.34 (2016-07-25) From 1aa5722c6a3d31201acecfe9e21c4d73892ac17d Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Tue, 6 Sep 2016 01:43:36 +0200 Subject: [PATCH 098/270] =?UTF-8?q?Fix=20conversion=20of=20cubic=20B=C3=A9?= =?UTF-8?q?zier=20"curveto"=20commands=20into=20"shorthand/smooth=20curvet?= =?UTF-8?q?o"=20commands.=20(#110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the preceeding path segment is a BĂ©zier curve, too, the first control point of the shorthand defaults to the mirrored version of the second control point of this preceeding path segment. Scour always assumed (0,0) as the control point in this case which could result in modified path data (e.g. #91). --- scour/scour.py | 8 +++++++- testscour.py | 8 ++++++-- unittests/path-bez-optimize.svg | 4 +++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 754e5dd..fdae854 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2193,7 +2193,13 @@ def cleanPath(element, options) : newPath.append( (cmd, lineTuples) ) # convert BĂ©zier curve segments into s where possible elif cmd == 'c': - bez_ctl_pt = (0,0) + # 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): diff --git a/testscour.py b/testscour.py index 294cb7f..9d9f460 100755 --- a/testscour.py +++ b/testscour.py @@ -675,9 +675,13 @@ class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase): class ChangeBezierToShorthandInPath(unittest.TestCase): def runTest(self): - path = scour.scourXmlFile('unittests/path-bez-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(path.getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', + 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): diff --git a/unittests/path-bez-optimize.svg b/unittests/path-bez-optimize.svg index 97bfdd1..30761f3 100644 --- a/unittests/path-bez-optimize.svg +++ b/unittests/path-bez-optimize.svg @@ -1,4 +1,6 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg version="1.1" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg"> - <path d="m10,100c50-50,50,50,100,0,50-50,50,50,100,0" fill="none" stroke="blue" stroke-width="5"/> + <path id="path1" d="m10,100c50-50,50,50,100,0,50-50,50,50,100,0" fill="none" stroke="blue" stroke-width="5"/> + <path id="path2a" d="m200,200c0,0 200,100 200,0" fill="none" stroke="red" stroke-width="5"/> + <path id="path2b" d="m0,300s200-100 200,0c0,0 200,100 200,0" fill="none" stroke="green" stroke-width="5"/> </svg> From 600ec2868c868e933dbadf80b438164f4b0690cf Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Tue, 6 Sep 2016 01:49:14 +0200 Subject: [PATCH 099/270] Update HISTORY.mdUpdate HISTORY.md --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index a15e345..7204120 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -20,6 +20,7 @@ * Improvement: Automatically order all attributes ([#105](https://github.com/scour-project/scour/issues/105)), as well as `style` declarations ([#107](https://github.com/scour-project/scour/issues/107)) allowing for a constant output across multiple runs of Scour. Before order could change arbitrarily. * Improve path scouring. ([#108](https://github.com/scour-project/scour/issues/108))<br>Notably Scour performs all caculations with enhanced precision now, guaranteeing maximum accuracy when optimizing path data. Numerical precision is reduced as a last step of the optimization according to the `--precision` option. * Fix replacement of removed duplicate gradients if the `fill`/`stroke` properties contained a fallback. ([#109](https://github.com/scour-project/scour/issues/109)) +* Fix conversion of cubic BĂ©zier "curveto" commands into "shorthand/smooth curveto" commands. ([#110](https://github.com/scour-project/scour/issues/110)) ## Version 0.34 (2016-07-25) From 1cde426009d802b98ecff7a4bcc71d559598870e Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 10 Sep 2016 22:08:49 +0200 Subject: [PATCH 100/270] Do not remove stroke-related styles if they might be inherited by a child (partially fixes #22) --- scour/scour.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index fdae854..992dc66 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1465,12 +1465,14 @@ def repairStyle(node, options): 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 : + if strokestyle in styleMap and not styleInheritedByChild(node, strokestyle): del styleMap[strokestyle] num += 1 # TODO: This is actually a problem if a parent element has a specified stroke # we need to properly calculate computed values - del styleMap['stroke'] + if not styleInheritedByChild(node, 'stroke'): + 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' : @@ -1562,6 +1564,55 @@ def repairStyle(node, options): return num +def styleInheritedByChild(node, style, nodeIsChild=False): + """ + 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 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 + + + 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 + + # 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 + + # 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 From 0b5eab7f2f3c48be584c1e2ab142cda4f3ce1e26 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 10 Sep 2016 22:13:02 +0200 Subject: [PATCH 101/270] Avoid removal of some additional styles if they might be inherited by a child --- scour/scour.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 992dc66..7ccf830 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1456,7 +1456,7 @@ def repairStyle(node, options): 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: + if uselessStyle in styleMap and not styleInheritedByChild(node, uselessStyle): del styleMap[uselessStyle] num += 1 @@ -1477,7 +1477,7 @@ def repairStyle(node, options): # 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 : + if fillstyle in styleMap and not styleInheritedByChild(node, fillstyle): del styleMap[fillstyle] num += 1 @@ -1486,7 +1486,7 @@ def repairStyle(node, options): fillOpacity = float(styleMap['fill-opacity']) if fillOpacity == 0.0 : for uselessFillStyle in [ 'fill', 'fill-rule' ] : - if uselessFillStyle in styleMap: + if uselessFillStyle in styleMap and not styleInheritedByChild(node, uselessFillStyle): del styleMap[uselessFillStyle] num += 1 @@ -1496,7 +1496,7 @@ def repairStyle(node, options): if strokeOpacity == 0.0 : for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset' ] : - if uselessStrokeStyle in styleMap: + if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): del styleMap[uselessStrokeStyle] num += 1 @@ -1506,7 +1506,7 @@ def repairStyle(node, options): if strokeWidth.value == 0.0 : for uselessStrokeStyle in [ 'stroke', 'stroke-linejoin', 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity' ] : - if uselessStrokeStyle in styleMap: + if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): del styleMap[uselessStrokeStyle] num += 1 From 0b5bb5184c5d3a16c907bc1a842b81f2006ba332 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 10 Sep 2016 22:59:29 +0200 Subject: [PATCH 102/270] Do not remove `stroke:none;` if a differing value would be inherited from a parent (fixes rest of #22) --- scour/scour.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 7ccf830..66795f6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1468,11 +1468,11 @@ def repairStyle(node, options): if strokestyle in styleMap and not styleInheritedByChild(node, strokestyle): del styleMap[strokestyle] num += 1 - # TODO: This is actually a problem if a parent element has a specified stroke # we need to properly calculate computed values if not styleInheritedByChild(node, 'stroke'): - del styleMap['stroke'] - num += 1 + 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' : @@ -1564,6 +1564,34 @@ def repairStyle(node, options): return num +def styleInheritedFromParent(node, style): + """ + 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; + + # 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 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) + def styleInheritedByChild(node, style, nodeIsChild=False): """ Returns whether 'style' is inherited by any children of the passed-in node From 10e687b88784ea14d1f85ba4c5be90ff433a26f2 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 11 Sep 2016 00:16:17 +0200 Subject: [PATCH 103/270] Add unittests for 1cde426009d802b98ecff7a4bcc71d559598870e and 641d2db08a392e4a7df20c700e1accb9cd8d1341 --- testscour.py | 18 ++++++++++++++++++ unittests/stroke-none.svg | 14 +++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/testscour.py b/testscour.py index 9d9f460..e97f252 100755 --- a/testscour.py +++ b/testscour.py @@ -495,12 +495,30 @@ class RemoveStrokeWhenStrokeNone(unittest.TestCase): 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' ) + +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' ) + 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' ) +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' ) + class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/stroke-none.svg') diff --git a/unittests/stroke-none.svg b/unittests/stroke-none.svg index 4582a85..84f6c66 100644 --- a/unittests/stroke-none.svg +++ b/unittests/stroke-none.svg @@ -1,4 +1,16 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg"> - <path id="p" fill="black" style="stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-dasharray: none; stroke-dashoffset: 2; stroke-linejoin: miter; stroke-opacity: 1;" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" /> + <path id="p" fill="black" style="stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-dasharray: none; stroke-dashoffset: 2; stroke-linejoin: miter; stroke-opacity: 1;" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" /> + + <g id="g1" style="stroke:#000"> + <path id="p0" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" /> + <path id="p1" style="stroke:none" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" /> + <g id="g2" style="stroke:none"> + <path id="p2" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" /> + </g> + </g> + + <g id="g3" style="stroke:none;stroke-width:1px"> + <path id="p3" style="stroke:#000" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" /> + </g> </svg> From b0651371877b79406c0c1d3395d0005236decc08 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 11 Sep 2016 01:09:30 +0200 Subject: [PATCH 104/270] Whitespace fix --- scour/scour.py | 160 ++++++++++++++++++++++++------------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 66795f6..51cc248 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -498,9 +498,9 @@ class SVGLength(object): if int(self.value) == self.value: self.value = int(self.value) - if unitBegin != 0 : + if unitBegin != 0: unitMatch = unit.search(str, unitBegin) - if unitMatch != None : + if unitMatch != None: self.units = Unit.get(unitMatch.group(0)) # invalid @@ -516,13 +516,13 @@ def findElementsWithId(node, elems=None): if elems is None: elems = {} id = node.getAttribute('id') - if id != '' : + if id != '': elems[id] = node - if node.hasChildNodes() : + 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 : + if child.nodeType == 1: findElementsWithId(child, elems) return elems @@ -578,38 +578,38 @@ def findReferencedElements(node, ids=None): for style in styles: propval = style.split(':') - if len(propval) == 2 : + if len(propval) == 2: prop = propval[0].strip() val = propval[1].strip() findReferencingProperty(node, prop, val, ids) - if node.hasChildNodes() : + if node.hasChildNodes(): for child in node.childNodes: - if child.nodeType == 1 : + 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(#' : + if prop in referencingProps and val != '': + if len(val) >= 7 and val[0:5] == 'url(#': id = val[5:val.find(')')] - if id in ids : + 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 : + elif len(val) >= 8: id = None # double-quote - if val[0:6] == 'url("#' : + if val[0:6] == 'url("#': id = val[6:val.find('")')] # single-quote - elif val[0:6] == "url('#" : + elif val[0:6] == "url('#": id = val[6:val.find("')")] if id != None: - if id in ids : + if id in ids: ids[id][0] += 1 ids[id][1].append(node) else: @@ -867,7 +867,7 @@ def removeUnreferencedIDs(referencedIDs, identifiedElements): def removeNamespacedAttributes(node, namespaces): global numAttrsRemoved num = 0 - if node.nodeType == 1 : + if node.nodeType == 1: # remove all namespace'd attributes from this element attrList = node.attributes attrsToRemove = [] @@ -875,7 +875,7 @@ def removeNamespacedAttributes(node, namespaces): attr = attrList.item(attrNum) if attr != None and attr.namespaceURI in namespaces: attrsToRemove.append(attr.nodeName) - for attrName in attrsToRemove : + for attrName in attrsToRemove: num += 1 numAttrsRemoved += 1 node.removeAttribute(attrName) @@ -888,14 +888,14 @@ def removeNamespacedAttributes(node, namespaces): def removeNamespacedElements(node, namespaces): global numElemsRemoved num = 0 - if node.nodeType == 1 : + 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 : + for child in childrenToRemove: num += 1 numElemsRemoved += 1 node.removeChild(child) @@ -1247,7 +1247,7 @@ def removeDuplicateGradientStops(doc): color = stop.getAttribute('stop-color') opacity = stop.getAttribute('stop-opacity') style = stop.getAttribute('style') - if offset in stops : + if offset in stops: oldStop = stops[offset] if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: stopsToRemove.append(stop) @@ -1413,12 +1413,12 @@ def removeDuplicateGradients(doc): def _getStyle(node): u"""Returns the style attribute of a node as a dictionary.""" - if node.nodeType == 1 and len(node.getAttribute('style')) > 0 : + 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 : + if len(propval) == 2: styleMap[propval[0].strip()] = propval[1].strip() return styleMap else: @@ -1427,7 +1427,7 @@ def _getStyle(node): 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 != '' : + if fixedStyle != '': node.setAttribute('style', fixedStyle) elif node.getAttribute('style'): node.removeAttribute('style') @@ -1440,31 +1440,31 @@ def repairStyle(node, options): # 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 : + 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 # Here is where we can weed out unnecessary styles like: # opacity:1 - if 'opacity' in styleMap : + 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 : + 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'] : + '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' : + 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'] : + 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity']: if strokestyle in styleMap and not styleInheritedByChild(node, strokestyle): del styleMap[strokestyle] num += 1 @@ -1475,37 +1475,37 @@ def repairStyle(node, options): 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 '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 : + if 'fill-opacity' in styleMap: fillOpacity = float(styleMap['fill-opacity']) - if fillOpacity == 0.0 : - for uselessFillStyle in [ 'fill', 'fill-rule' ] : + 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 : + if 'stroke-opacity' in styleMap: strokeOpacity = float(styleMap['stroke-opacity']) - if strokeOpacity == 0.0 : + if strokeOpacity == 0.0: for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset' ] : + '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 : + if 'stroke-width' in styleMap: strokeWidth = SVGLength(styleMap['stroke-width']) - if strokeWidth.value == 0.0 : + if strokeWidth.value == 0.0: for uselessStrokeStyle in [ 'stroke', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity' ] : + 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity' ]: if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): del styleMap[uselessStrokeStyle] num += 1 @@ -1518,8 +1518,8 @@ def repairStyle(node, options): 'letter-spacing', 'line-height', 'kerning', 'text-align', 'text-anchor', 'text-decoration', 'text-rendering', 'unicode-bidi', - 'word-spacing', 'writing-mode'] : - if fontstyle in styleMap : + 'word-spacing', 'writing-mode']: + if fontstyle in styleMap: del styleMap[fontstyle] num += 1 @@ -1530,7 +1530,7 @@ def repairStyle(node, options): del styleMap[inkscapeStyle] num += 1 - if 'overflow' in styleMap : + 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']: @@ -1551,15 +1551,15 @@ def repairStyle(node, options): # 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 : + for propName in list(styleMap.keys()): + if propName in svgAttributes: node.setAttribute(propName, styleMap[propName]) del styleMap[propName] _setStyle(node, styleMap) # recurse for our child elements - for child in node.childNodes : + for child in node.childNodes: num += repairStyle(child,options) return num @@ -1567,7 +1567,7 @@ def repairStyle(node, options): def styleInheritedFromParent(node, style): """ 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! """ @@ -1583,7 +1583,7 @@ def styleInheritedFromParent(node, style): value = styles[style] if not value == 'inherit': return value - + # check attributes value = parentNode.getAttribute(style) if value not in ['', 'inherit']: @@ -1601,7 +1601,7 @@ def styleInheritedByChild(node, style, nodeIsChild=False): 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! """ @@ -1609,7 +1609,7 @@ def styleInheritedByChild(node, style, nodeIsChild=False): 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 @@ -1885,7 +1885,7 @@ def removeDefaultAttributeValues(node, options, tainted=set()): _setStyle(node, styles) # recurse for our child elements - for child in node.childNodes : + for child in node.childNodes: num += removeDefaultAttributeValues(child, options, tainted.copy()) return num @@ -1902,14 +1902,14 @@ def convertColor(value): s = colors[s] rgbpMatch = rgbp.match(s) - if rgbpMatch != None : + 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 : + if rgbMatch != None: r = int( rgbMatch.group(1) ) g = int( rgbMatch.group(2) ) b = int( rgbMatch.group(3) ) @@ -1922,7 +1922,7 @@ def convertColor(value): return s -def convertColors(element) : +def convertColors(element): """ Recursively converts all color properties into #RRGGBB format if shorter """ @@ -1963,7 +1963,7 @@ def convertColors(element) : _setStyle(element, styles) # now recurse for our child elements - for child in element.childNodes : + for child in element.childNodes: numBytes += convertColors(child) return numBytes @@ -1971,7 +1971,7 @@ def convertColors(element) : # 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) : +def cleanPath(element, options): """ Cleans the path string (d attribute) of the element """ @@ -2589,7 +2589,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a # 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: + 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? @@ -2606,7 +2606,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a -def reducePrecision(element) : +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. @@ -2837,7 +2837,7 @@ def optimizeTransform(transform): -def optimizeTransforms(element, options) : +def optimizeTransforms(element, options): """ Attempts to optimise transform specifications on the given node and its children. @@ -2869,7 +2869,7 @@ def optimizeTransforms(element, options) : -def removeComments(element) : +def removeComments(element): """ Removes comments from the element and its children. """ @@ -2884,7 +2884,7 @@ def removeComments(element) : -def embedRasters(element, options) : +def embedRasters(element, options): import base64 import urllib """ @@ -2926,7 +2926,7 @@ def embedRasters(element, options) : webFile.close() # ... should we remove all images which don't resolve? - if rasterdata != '' : + if rasterdata != '': # base64-encode raster b64eRaster = base64.b64encode( rasterdata ) @@ -3017,7 +3017,7 @@ def remapNamespacePrefix(node, oldprefix, newprefix): node = newNode # now do all child nodes - for child in node.childNodes : + for child in node.childNodes: remapNamespacePrefix(child, oldprefix, newprefix) @@ -3094,7 +3094,7 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): attrIndices.append(attrName2Index[name]) del attrName2Index[name] attrIndices += [attrName2Index[name] for name in sorted(attrName2Index.keys())] - for index in attrIndices : + 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 @@ -3206,19 +3206,19 @@ def scourString(in_string, options=None): # 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 : + while removeNamespacedElements( doc.documentElement, unwanted_ns ) > 0: pass - while removeNamespacedAttributes( doc.documentElement, unwanted_ns ) > 0 : + 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 : + for index in range(attrList.length): + if attrList.item(index).nodeValue in unwanted_ns: xmlnsDeclsToRemove.append(attrList.item(index).nodeName) - for attr in xmlnsDeclsToRemove : + for attr in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attr) numAttrsRemoved += 1 @@ -3281,18 +3281,18 @@ def scourString(in_string, options=None): # 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) : + 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 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 : + if removeElem: elem.parentNode.removeChild(elem) numElemsRemoved += 1 @@ -3338,15 +3338,15 @@ def scourString(in_string, options=None): pass # remove unnecessary closing point of polygons and scour points - for polygon in doc.documentElement.getElementsByTagName('polygon') : + for polygon in doc.documentElement.getElementsByTagName('polygon'): cleanPolygon(polygon, options) # scour points of polyline - for polyline in doc.documentElement.getElementsByTagName('polyline') : + for polyline in doc.documentElement.getElementsByTagName('polyline'): cleanPolyline(polyline, options) # clean path data - for elem in doc.documentElement.getElementsByTagName('path') : + for elem in doc.documentElement.getElementsByTagName('path'): if elem.getAttribute('d') == '': elem.parentNode.removeChild(elem) else: @@ -3375,7 +3375,7 @@ def scourString(in_string, options=None): # convert rasters references to base64-encoded strings if options.embed_rasters: - for elem in doc.documentElement.getElementsByTagName('image') : + for elem in doc.documentElement.getElementsByTagName('image'): embedRasters(elem, options) # properly size the SVG document (ideally width/height should be 100% with a viewBox) From 082b579410a852dd1001115609773519e8d20dce Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 11 Sep 2016 14:01:04 +0200 Subject: [PATCH 105/270] We don't want spaces in the serialized value of `style` attributes. Add a unittest that should catch this and other issues with parsing/serializing the `style` attribute --- scour/scour.py | 2 +- testscour.py | 7 +++++++ unittests/style.svg | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 unittests/style.svg diff --git a/scour/scour.py b/scour/scour.py index 51cc248..da212fb 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3105,7 +3105,7 @@ def serializeXML(element, options, ind = 0, preserveWhitespace = False): attrValue = makeWellFormed( attr.nodeValue ) if attr.nodeName == 'style': # sort declarations - attrValue = '; '.join([p for p in sorted(attrValue.split(';'))]) + attrValue = ';'.join([p for p in sorted(attrValue.split(';'))]) outParts.append(' ') # preserve xmlns: if it is a namespace prefix declaration diff --git a/testscour.py b/testscour.py index e97f252..28b3148 100755 --- a/testscour.py +++ b/testscour.py @@ -1589,6 +1589,13 @@ class DocWithNoFlowtext(unittest.TestCase): except Exception as e: self.fail("exception '{}' was raised, and we didn't expect that!".format(e)) + +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') + # TODO: write tests for --enable-viewboxing # TODO; write a test for embedding rasters # TODO: write a test for --disable-embed-rasters diff --git a/unittests/style.svg b/unittests/style.svg new file mode 100644 index 0000000..dfe5b8d --- /dev/null +++ b/unittests/style.svg @@ -0,0 +1,2 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" style=" ; ;;property1: value1 ; ;; property2 :value2; property3 : value3 ;;; ;"></svg> From 9629e87f37c8f38feee32cfe2c17b71dcf6369d9 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 11 Sep 2016 14:03:31 +0200 Subject: [PATCH 106/270] Throw some line breaks in there... --- unittests/style.svg | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/unittests/style.svg b/unittests/style.svg index dfe5b8d..2148103 100644 --- a/unittests/style.svg +++ b/unittests/style.svg @@ -1,2 +1,7 @@ <?xml version="1.0"?> -<svg xmlns="http://www.w3.org/2000/svg" style=" ; ;;property1: value1 ; ;; property2 :value2; property3 : value3 ;;; ;"></svg> +<svg xmlns="http://www.w3.org/2000/svg" style=" ; ;;property1: + + + value1 ; ;; property2 :value2; property3 : value3 ; ; + + ; ;"></svg> From 9b80fc55a2fca2b887409e1e5fe66d257ac31ae7 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 11 Sep 2016 14:10:24 +0200 Subject: [PATCH 107/270] Update HISTORY.md --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index 7204120..8f8a51e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -21,6 +21,7 @@ * Improve path scouring. ([#108](https://github.com/scour-project/scour/issues/108))<br>Notably Scour performs all caculations with enhanced precision now, guaranteeing maximum accuracy when optimizing path data. Numerical precision is reduced as a last step of the optimization according to the `--precision` option. * Fix replacement of removed duplicate gradients if the `fill`/`stroke` properties contained a fallback. ([#109](https://github.com/scour-project/scour/issues/109)) * Fix conversion of cubic BĂ©zier "curveto" commands into "shorthand/smooth curveto" commands. ([#110](https://github.com/scour-project/scour/issues/110)) +* Fix some issues due to removal of properties without considering inheritance rules. ([#111](https://github.com/scour-project/scour/issues/111)) ## Version 0.34 (2016-07-25) From 943319b7107e5cd23c31d2aaa7f9dec5eaec931a Mon Sep 17 00:00:00 2001 From: Tobias Oberstein <tobias.oberstein@tavendo.de> Date: Wed, 14 Sep 2016 12:10:57 +0200 Subject: [PATCH 108/270] bump version --- scour/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/__init__.py b/scour/__init__.py index 8ba6dd7..568336e 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ ## ############################################################################### -__version__ = u'0.34' +__version__ = u'0.35' From 0f1974c1e230b789eaab36b105aecf5af98fcf44 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 15 Sep 2016 00:35:13 +0200 Subject: [PATCH 109/270] 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 <svg>, <font>, <metadata>, <title>, and <desc>. - Also vacuums the defs of any non-referenced renderable elements. + """ + Removes all unreferenced elements except for <svg>, <font>, <metadata>, <title>, and <desc>. + 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 <g> elements for promotion if this element isn't a <switch>. - # (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 <g> elements to contain runs of 3 or more - consecutive child elements having at least one common attribute. - - Common attributes are not promoted to the <g> by this function. - This is handled by moveCommonAttributesToParentGroup. - - If all children have a common attribute, an extra <g> 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 <g> - - # 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 <g> 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 <svg> element, and - # even if all children have the same attribute value, - # it's going to be worth making the <g> since - # <svg> doesn't support attributes like 'stroke'. - # Fall through. - - # Create a <g> 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 <g> elements for promotion if this element isn't a <switch>. + # (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 <g> elements to contain runs of 3 or more + consecutive child elements having at least one common attribute. + + Common attributes are not promoted to the <g> by this function. + This is handled by moveCommonAttributesToParentGroup. + + If all children have a common attribute, an extra <g> 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 <g> + + # 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 <g> 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 <svg> element, and + # even if all children have the same attribute value, + # it's going to be worth making the <g> since + # <svg> doesn't support attributes like 'stroke'. + # Fall through. + + # Create a <g> 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 <svg> 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 <svg> 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 <svg> 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 <svg> 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 - # <polygon points="100,-100,100-100,100-100-100,-100-100" /> - for i in range(len(ws_nums)): - negcoords = ws_nums[i].split("-") + # also, if 100-100 is found, split it into two also + # <polygon points="100,-100,100-100,100-100-100,-100-100" /> + 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(['<![CDATA[', child.nodeValue, ']]>']) - # Comment node - elif child.nodeType == 8: - outParts.extend(['<!--', child.nodeValue, '-->']) - # 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(['<![CDATA[', child.nodeValue, ']]>']) + # Comment node + elif child.nodeType == 8: + outParts.extend(['<!--', child.nodeValue, '-->']) + # TODO: entities, processing instructions, what else? + else: # ignore the rest + pass - if onNewLine: outParts.append(I * ind) - outParts.extend(['</', element.nodeName, '>']) - 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(['</', element.nodeName, '>']) + 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 <g> 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 <svg> element's immediate children + # all have the same value for an attribute, it must not + # get moved to the <svg> element. The <svg> 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 <g>'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 <g> 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 <svg> element's immediate children - # all have the same value for an attribute, it must not - # get moved to the <svg> element. The <svg> 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 <g>'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 = '<?xml version="1.0" encoding="UTF-8"' - if doc.standalone: - total_output += ' standalone="yes"' - total_output += '?>\n' - else: - total_output = "" + # return the string with its XML prolog and surrounding comments + if options.strip_xml_prolog == False: + total_output = '<?xml version="1.0" encoding="UTF-8"' + if doc.standalone: + total_output += ' standalone="yes"' + 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 <g> elements") + action="store_false", dest="group_collapse", default=True, + help="won't collapse <g> elements") _option_group_optimization.add_option("--create-groups", - action="store_true", dest="group_create", default=False, - help="create <g> elements for runs of elements with identical attributes") + action="store_true", dest="group_create", default=False, + 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") + 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 (<?xml ?>)") + action="store_true", dest="strip_xml_prolog", default=False, + help="won't output the XML prolog (<?xml ?>)") _option_group_document.add_option("--remove-titles", - action="store_true", dest="remove_titles", default=False, - help="remove <title> elements") + action="store_true", dest="remove_titles", default=False, + help="remove <title> 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 110/270] 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 111/270] 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 112/270] 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 113/270] 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 114/270] 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 From 79390dc0e4e12c3e199d7469b7ccff93942a5fcc Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Fri, 16 Sep 2016 02:03:03 +0200 Subject: [PATCH 115/270] Add `flake8` to automated tests --- .travis.yml | 8 ++++++-- tox.ini | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e423675..418ca7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,12 @@ install: - pip install tox env: -# - TOX_ENV=py26 + - TOX_ENV=pypy - TOX_ENV=py27 - TOX_ENV=py33 - TOX_ENV=py34 # - TOX_ENV=py35 - - TOX_ENV=pypy +# - TOX_ENV=flake8 script: - tox -e $TOX_ENV @@ -22,3 +22,7 @@ matrix: - python: 3.5 env: - TOX_ENV=py35 + + - python: 3.5 + env: + - TOX_ENV=flake8 diff --git a/tox.ini b/tox.ini index 2939feb..156850d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,23 @@ +[tox] +envlist = + pypy + py27 + py33 + py34 + py35 + flake8 + + + [testenv] commands = scour --version python testscour.py + + +[testenv:flake8] +deps = + flake8 + +commands = + flake8 --max-line-length=119 \ No newline at end of file From 42dc70874c593bc67fc599e76815bef1415cff52 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 17 Sep 2016 01:18:37 +0200 Subject: [PATCH 116/270] Automate `coverage` via Travis and Codecov --- .travis.yml | 5 ++++- tox.ini | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 418ca7a..48a15d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python install: - - pip install tox + - pip install tox codecov env: - TOX_ENV=pypy @@ -26,3 +26,6 @@ matrix: - python: 3.5 env: - TOX_ENV=flake8 + +after_success: + - coverage combine && codecov \ No newline at end of file diff --git a/tox.ini b/tox.ini index 156850d..30dacf8 100644 --- a/tox.ini +++ b/tox.ini @@ -10,9 +10,13 @@ envlist = [testenv] +deps = + six + coverage + commands = scour --version - python testscour.py + coverage run --parallel-mode --source=scour testscour.py [testenv:flake8] From 582c2dd9b7cc4521cff3baeff2881731e906d688 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 17 Sep 2016 02:26:13 +0200 Subject: [PATCH 117/270] Add `coverage` to Makefile and improve `clean` target --- Makefile | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cb4ab46..4f018c1 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,11 @@ clean: rm -rf build rm -rf dist rm -rf scour.egg-info + rm -rf .tox + rm -f .coverage* + rm -rf htmlcov + find . -name "*.pyc" -type f -exec rm -f {} \; + find . -name "*__pycache__" -type d -prune -exec rm -rf {} \; publish: clean python setup.py register @@ -25,4 +30,9 @@ test_error_on_flowtext: PYTHONPATH=. scour --error-on-flowtext unittests/flowtext.svg /dev/null flake8: - flake8 --max-line-length=119 \ No newline at end of file + flake8 --max-line-length=119 + +coverage: + coverage run --source=scour testscour.py + coverage html + coverage report \ No newline at end of file From 18970e0ba19194ff9ca5f2e3de34dfdfe1dedc75 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 17 Sep 2016 16:28:13 +0200 Subject: [PATCH 118/270] Update README.md - add codecov badge - exclusively use "shields.io" for badges (one service potentially being able to track visitors is enough) - add title texts --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ceab3f6..f181151 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Scour -**Build status:** [![Build Status](https://travis-ci.org/scour-project/scour.svg?branch=master)](https://travis-ci.org/scour-project/scour) +[![PyPI](https://img.shields.io/pypi/v/scour.svg)](https://pypi.python.org/pypi/scour "Package listing on PyPI") +†+[![Build status](https://img.shields.io/travis/scour-project/scour.svg)](https://travis-ci.org/scour-project/scour "Build status (via TravisCI)") +[![Codecov](https://img.shields.io/codecov/c/github/scour-project/scour.svg)](https://codecov.io/gh/scour-project/scour "Code coverage (via Codecov)") --- From 8ac344daf2b8357d7d2e5bdce628c03917b6161b Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 17 Sep 2016 17:09:35 +0200 Subject: [PATCH 119/270] Update Makefile - add `make` target to run unittests - add a convenience `check` target (that runs unittests and flake8 for now) - remove `test_error_on_flowtext` target (we have unittests for that now) --- Makefile | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 4f018c1..52323c1 100644 --- a/Makefile +++ b/Makefile @@ -17,18 +17,19 @@ publish: clean python setup.py register python setup.py sdist upload +check: test flake8 + + + +test: + python testscour.py + test_version: PYTHONPATH=. python -m scour.scour --version test_help: PYTHONPATH=. python -m scour.scour --help -test_error_on_flowtext: - # this is fine .. - 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 From 4a5b924d375461444c4fe2f61942a3febf37af1d Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 18 Sep 2016 03:07:51 +0200 Subject: [PATCH 120/270] Do not attempt to close stdin/stdout file objects --- scour/scour.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 8f32367..5801962 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3758,9 +3758,13 @@ def start(options, input, output): 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 (but do not attempt to close stdin/stdout!) + if input is not sys.stdin: + if hasattr(input, 'buffer') and input is not sys.stdin.buffer: + input.close() + if output is not sys.stdout: + if hasattr(output, 'buffer') and output is not sys.stdout.buffer: + output.close() end = walltime() From 4410f91dad3a780f52da75bff0103832d7ed7f2b Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 18 Sep 2016 03:25:19 +0200 Subject: [PATCH 121/270] Fix logic in previous commit --- scour/scour.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 5801962..13cc57e 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3759,12 +3759,10 @@ def start(options, input, output): output.write(out_string) # Close input and output files (but do not attempt to close stdin/stdout!) - if input is not sys.stdin: - if hasattr(input, 'buffer') and input is not sys.stdin.buffer: - input.close() - if output is not sys.stdout: - if hasattr(output, 'buffer') and output is not sys.stdout.buffer: - output.close() + if not ((input is sys.stdin) or (hasattr(input, 'buffer') and input is sys.stdin.buffer)): + input.close() + if not ((output is sys.stdout) or (hasattr(output, 'buffer') and output is sys.stdout.buffer)): + output.close() end = walltime() From 2487f4433b9d8c4b97f7cf32361eb5e960e4f012 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 18 Sep 2016 16:23:00 +0200 Subject: [PATCH 122/270] Fixes to globals used for tracking statistics (#118) - Collect globals in `scourString()` and make sure they're all properly initialized to zero. Before statistics were wrong when scouring multiple files/strings because initialization was only done once when loading the module. - harmonize names - adjust according to PEP 8 while at it (including leading underscore to mark as non-public) - include one missing variable in statistics output (number of comments removed) --- scour/scour.py | 200 ++++++++++++++++++++++++++----------------------- 1 file changed, 105 insertions(+), 95 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 13cc57e..80923f8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -534,8 +534,8 @@ def findElementsWithId(node, elems=None): findElementsWithId(child, elems) return elems -referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', - 'marker-end', 'marker-mid'] + +referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-end', 'marker-mid'] def findReferencedElements(node, ids=None): @@ -625,20 +625,6 @@ def findReferencingProperty(node, prop, val, ids): else: ids[id] = [1, [node]] -numIDsRemoved = 0 -numElemsRemoved = 0 -numAttrsRemoved = 0 -numRastersEmbedded = 0 -numPathSegmentsReduced = 0 -numCurvesStraightened = 0 -numBytesSavedInPathData = 0 -numBytesSavedInColors = 0 -numBytesSavedInIDs = 0 -numBytesSavedInLengths = 0 -numBytesSavedInTransforms = 0 -numPointsRemovedFromPolygon = 0 -numCommentBytes = 0 - def removeUnusedDefs(doc, defElem, elemsToRemove=None): if elemsToRemove is None: @@ -668,7 +654,7 @@ def removeUnreferencedElements(doc, keepDefs): Returns the number of unreferenced elements removed from the document. """ - global numElemsRemoved + global _num_elements_removed num = 0 # Remove certain unreferenced elements outside of defs @@ -684,7 +670,7 @@ def removeUnreferencedElements(doc, keepDefs): and goner.parentNode.tagName != 'defs'): goner.parentNode.removeChild(goner) num += 1 - numElemsRemoved += 1 + _num_elements_removed += 1 if not keepDefs: # Remove most unreferenced elements inside defs @@ -693,7 +679,7 @@ def removeUnreferencedElements(doc, keepDefs): elemsToRemove = removeUnusedDefs(doc, aDef) for elem in elemsToRemove: elem.parentNode.removeChild(elem) - numElemsRemoved += 1 + _num_elements_removed += 1 num += 1 return num @@ -868,20 +854,20 @@ def removeUnreferencedIDs(referencedIDs, identifiedElements): Returns the number of ID attributes removed """ - global numIDsRemoved + global _num_ids_removed keepTags = ['font'] num = 0 for id in list(identifiedElements.keys()): node = identifiedElements[id] if id not in referencedIDs and node.nodeName not in keepTags: node.removeAttribute('id') - numIDsRemoved += 1 + _num_ids_removed += 1 num += 1 return num def removeNamespacedAttributes(node, namespaces): - global numAttrsRemoved + global _num_attributes_removed num = 0 if node.nodeType == 1: # remove all namespace'd attributes from this element @@ -893,7 +879,7 @@ def removeNamespacedAttributes(node, namespaces): attrsToRemove.append(attr.nodeName) for attrName in attrsToRemove: num += 1 - numAttrsRemoved += 1 + _num_attributes_removed += 1 node.removeAttribute(attrName) # now recurse for children @@ -903,7 +889,7 @@ def removeNamespacedAttributes(node, namespaces): def removeNamespacedElements(node, namespaces): - global numElemsRemoved + global _num_elements_removed num = 0 if node.nodeType == 1: # remove all namespace'd child nodes from this element @@ -914,7 +900,7 @@ def removeNamespacedElements(node, namespaces): childrenToRemove.append(child) for child in childrenToRemove: num += 1 - numElemsRemoved += 1 + _num_elements_removed += 1 node.removeChild(child) # now recurse for children @@ -937,7 +923,7 @@ def removeDescriptiveElements(doc, options): if not elementTypes: return - global numElemsRemoved + global _num_elements_removed num = 0 elementsToRemove = [] for elementType in elementTypes: @@ -946,7 +932,7 @@ def removeDescriptiveElements(doc, options): for element in elementsToRemove: element.parentNode.removeChild(element) num += 1 - numElemsRemoved += 1 + _num_elements_removed += 1 return num @@ -957,7 +943,7 @@ def removeNestedGroups(node): which do not have any attributes or a title/desc child and promoting their children up one level """ - global numElemsRemoved + global _num_elements_removed num = 0 groupsToRemove = [] @@ -978,7 +964,7 @@ def removeNestedGroups(node): while g.childNodes.length > 0: g.parentNode.insertBefore(g.firstChild, g) g.parentNode.removeChild(g) - numElemsRemoved += 1 + _num_elements_removed += 1 num += 1 # now recurse for children @@ -1084,7 +1070,7 @@ def createGroupsForCommonAttributes(elem): This function acts recursively on the given element. """ num = 0 - global numElemsRemoved + global _num_elements_removed # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement # could be added here @@ -1183,7 +1169,7 @@ def createGroupsForCommonAttributes(elem): group.parentNode = elem num += 1 curChild = runStart - 1 - numElemsRemoved -= 1 + _num_elements_removed -= 1 else: curChild -= 1 else: @@ -1253,7 +1239,7 @@ def removeUnusedAttributesOnParent(elem): def removeDuplicateGradientStops(doc): - global numElemsRemoved + global _num_elements_removed num = 0 for gradType in ['linearGradient', 'radialGradient']: @@ -1287,14 +1273,14 @@ def removeDuplicateGradientStops(doc): for stop in stopsToRemove: stop.parentNode.removeChild(stop) num += 1 - numElemsRemoved += 1 + _num_elements_removed += 1 # linear gradients return num def collapseSinglyReferencedGradients(doc): - global numElemsRemoved + global _num_elements_removed num = 0 identifiedElements = findElementsWithId(doc.documentElement) @@ -1346,13 +1332,13 @@ def collapseSinglyReferencedGradients(doc): # now delete elem elem.parentNode.removeChild(elem) - numElemsRemoved += 1 + _num_elements_removed += 1 num += 1 return num def removeDuplicateGradients(doc): - global numElemsRemoved + global _num_elements_removed num = 0 gradientsToRemove = {} @@ -1446,7 +1432,7 @@ def removeDuplicateGradients(doc): # 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_elements_removed += 1 num += 1 return num @@ -2051,9 +2037,8 @@ def cleanPath(element, options): """ Cleans the path string (d attribute) of the element """ - global numBytesSavedInPathData - global numPathSegmentsReduced - global numCurvesStraightened + global _num_bytes_saved_in_path_data + global _num_path_segments_removed # this gets the parser object from svg_regex.py oldPathStr = element.getAttribute('d') @@ -2183,40 +2168,40 @@ def cleanPath(element, options): # '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 + _num_path_segments_removed += 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 + _num_path_segments_removed += 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 + _num_path_segments_removed += 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 + _num_path_segments_removed += 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 + _num_path_segments_removed += 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 + _num_path_segments_removed += 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'] @@ -2254,7 +2239,6 @@ def cleanPath(element, options): newData = [] # now create a straight line segment newPath.append(('l', [dx, dy])) - numCurvesStraightened += 1 else: newData.extend(data[i:i + 6]) @@ -2306,14 +2290,14 @@ def cleanPath(element, options): lineTuples = [] # append the v and then the remaining line coords newPath.append(('v', [data[i + 1]])) - numPathSegmentsReduced += 1 + _num_path_segments_removed += 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 + _num_path_segments_removed += 1 else: lineTuples.extend(data[i:i + 2]) i += 2 @@ -2333,7 +2317,7 @@ def cleanPath(element, options): cmd = 'l' # dealing with linetos now # append the v and then the remaining line coords newPath.append(('v', [data[i + 1]])) - numPathSegmentsReduced += 1 + _num_path_segments_removed += 1 elif data[i + 1] == 0: if lineTuples: # flush the m/l command, then append the h and then the remaining line coords @@ -2341,7 +2325,7 @@ def cleanPath(element, options): lineTuples = [] cmd = 'l' # dealing with linetos now newPath.append(('h', [data[i]])) - numPathSegmentsReduced += 1 + _num_path_segments_removed += 1 else: lineTuples.extend(data[i:i + 2]) i += 2 @@ -2370,7 +2354,7 @@ def cleanPath(element, options): curveTuples = [] # append the s command newPath.append(('s', [data[i + 2], data[i + 3], data[i + 4], data[i + 5]])) - numPathSegmentsReduced += 1 + _num_path_segments_removed += 1 else: j = 0 while j <= 5: @@ -2395,7 +2379,7 @@ def cleanPath(element, options): curveTuples = [] # append the t command newPath.append(('t', [data[i + 2], data[i + 3]])) - numPathSegmentsReduced += 1 + _num_path_segments_removed += 1 else: j = 0 while j <= 3: @@ -2424,7 +2408,7 @@ def cleanPath(element, options): if isSameSign(data[coordIndex - 1], data[coordIndex]): data[coordIndex - 1] += data[coordIndex] del data[coordIndex] - numPathSegmentsReduced += 1 + _num_path_segments_removed += 1 else: coordIndex += 1 @@ -2459,7 +2443,7 @@ def cleanPath(element, 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)) + _num_bytes_saved_in_path_data += (len(oldPathStr) - len(newPathStr)) element.setAttribute('d', newPathStr) @@ -2527,7 +2511,7 @@ def cleanPolygon(elem, options): """ Remove unnecessary closing point of polygon points attribute """ - global numPointsRemovedFromPolygon + global _num_points_removed_from_polygon pts = parseListOfPoints(elem.getAttribute('points')) N = len(pts) / 2 @@ -2536,7 +2520,7 @@ def cleanPolygon(elem, options): (endx, endy) = pts[-2:] if startx == endx and starty == endy: del pts[-2:] - numPointsRemovedFromPolygon += 1 + _num_points_removed_from_polygon += 1 elem.setAttribute('points', scourCoordinates(pts, options, True)) @@ -2944,10 +2928,10 @@ def removeComments(element): """ Removes comments from the element and its children. """ - global numCommentBytes + global _num_bytes_saved_in_comments if isinstance(element, xml.dom.minidom.Comment): - numCommentBytes += len(element.data) + _num_bytes_saved_in_comments += len(element.data) element.parentNode.removeChild(element) else: for subelement in element.childNodes[:]: @@ -2961,7 +2945,7 @@ def embedRasters(element, options): Converts raster references to inline images. NOTE: there are size limits to base64-encoding handling in browsers """ - global numRastersEmbedded + global _num_rasters_embedded href = element.getAttributeNS(NS['XLINK'], 'href') @@ -3009,7 +2993,7 @@ def embedRasters(element, options): ext = 'jpeg' element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster) - numRastersEmbedded += 1 + _num_rasters_embedded += 1 del b64eRaster @@ -3253,14 +3237,37 @@ def scourString(in_string, options=None): global scouringContext scouringContext = Context(prec=options.digits) - global numAttrsRemoved - global numStylePropsFixed - global numElemsRemoved - global numBytesSavedInColors - global numCommentsRemoved - global numBytesSavedInIDs - global numBytesSavedInLengths - global numBytesSavedInTransforms + # globals for tracking statistics + # TODO: get rid of these globals... + global _num_elements_removed + global _num_attributes_removed + global _num_ids_removed + global _num_comments_removed + global _num_style_properties_fixed + global _num_rasters_embedded + global _num_path_segments_removed + global _num_points_removed_from_polygon + global _num_bytes_saved_in_path_data + global _num_bytes_saved_in_colors + global _num_bytes_saved_in_comments + global _num_bytes_saved_in_ids + global _num_bytes_saved_in_lengths + global _num_bytes_saved_in_transforms + _num_elements_removed = 0 + _num_attributes_removed = 0 + _num_ids_removed = 0 + _num_comments_removed = 0 + _num_style_properties_fixed = 0 + _num_rasters_embedded = 0 + _num_path_segments_removed = 0 + _num_points_removed_from_polygon = 0 + _num_bytes_saved_in_path_data = 0 + _num_bytes_saved_in_colors = 0 + _num_bytes_saved_in_comments = 0 + _num_bytes_saved_in_ids = 0 + _num_bytes_saved_in_lengths = 0 + _num_bytes_saved_in_transforms = 0 + doc = xml.dom.minidom.parseString(in_string) # determine number of flowRoot elements in input document @@ -3295,7 +3302,7 @@ def scourString(in_string, options=None): for attr in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attr) - numAttrsRemoved += 1 + _num_attributes_removed += 1 # ensure namespace for SVG is declared # TODO: what if the default namespace is something else (i.e. some valid namespace)? @@ -3330,24 +3337,24 @@ def scourString(in_string, options=None): for attrName in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attrName) - numAttrsRemoved += 1 + _num_attributes_removed += 1 for prefix in redundantPrefixes: remapNamespacePrefix(doc.documentElement, prefix, '') if options.strip_comments: - numCommentsRemoved = removeComments(doc) + _num_comments_removed = removeComments(doc) if options.strip_xml_space_attribute and doc.documentElement.hasAttribute('xml:space'): doc.documentElement.removeAttribute('xml:space') - numAttrsRemoved += 1 + _num_attributes_removed += 1 # repair style (remove unnecessary style properties and change them into XML attributes) - numStylePropsFixed = repairStyle(doc.documentElement, options) + _num_style_properties_fixed = repairStyle(doc.documentElement, options) # convert colors to #RRGGBB format if options.simple_colors: - numBytesSavedInColors = convertColors(doc.documentElement) + _num_bytes_saved_in_colors = convertColors(doc.documentElement) # remove unreferenced gradients/patterns outside of defs # and most unreferenced elements inside of defs @@ -3369,7 +3376,7 @@ def scourString(in_string, options=None): removeElem = True if removeElem: elem.parentNode.removeChild(elem) - numElemsRemoved += 1 + _num_elements_removed += 1 if options.strip_ids: bContinueLooping = True @@ -3401,10 +3408,10 @@ def scourString(in_string, options=None): # doesn't accept fill=, stroke= etc.! referencedIds = findReferencedElements(doc.documentElement) for child in doc.documentElement.childNodes: - numAttrsRemoved += moveCommonAttributesToParentGroup(child, referencedIds) + _num_attributes_removed += moveCommonAttributesToParentGroup(child, referencedIds) # remove unused attributes from parent - numAttrsRemoved += removeUnusedAttributesOnParent(doc.documentElement) + _num_attributes_removed += removeUnusedAttributesOnParent(doc.documentElement) # Collapse groups LAST, because we've created groups. If done before # moveAttributesToParentGroup, empty <g>'s may remain. @@ -3429,7 +3436,7 @@ def scourString(in_string, options=None): # shorten ID names as much as possible if options.shorten_ids: - numBytesSavedInIDs += shortenIDs(doc, options.shorten_ids_prefix, unprotected_ids(doc, options)) + _num_bytes_saved_in_ids += shortenIDs(doc, options.shorten_ids_prefix, unprotected_ids(doc, options)) # scour lengths (including coordinates) for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', @@ -3441,13 +3448,13 @@ def scourString(in_string, options=None): elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) # more length scouring in this function - numBytesSavedInLengths = reducePrecision(doc.documentElement) + _num_bytes_saved_in_lengths = reducePrecision(doc.documentElement) # remove default values of attributes - numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) + _num_attributes_removed += removeDefaultAttributeValues(doc.documentElement, options) # reduce the length of transformation attributes - numBytesSavedInTransforms = optimizeTransforms(doc.documentElement, options) + _num_bytes_saved_in_transforms = optimizeTransforms(doc.documentElement, options) # convert rasters references to base64-encoded strings if options.embed_rasters: @@ -3734,19 +3741,22 @@ def getInOut(options): 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(_num_elements_removed) + os.linesep + + ' Number of attributes removed: ' + str(_num_attributes_removed) + os.linesep + + ' Number of unreferenced IDs removed: ' + str(_num_ids_removed) + os.linesep + + ' Number of comments removed: ' + str(_num_comments_removed) + os.linesep + + ' Number of style properties fixed: ' + str(_num_style_properties_fixed) + os.linesep + + ' Number of raster images embedded: ' + str(_num_rasters_embedded) + os.linesep + + ' Number of path segments reduced/removed: ' + str(_num_path_segments_removed) + os.linesep + + ' Number of points removed from polygons: ' + str(_num_points_removed_from_polygon) + os.linesep + + ' Number of bytes saved in path data: ' + str(_num_bytes_saved_in_path_data) + os.linesep + + ' Number of bytes saved in colors: ' + str(_num_bytes_saved_in_colors) + os.linesep + + ' Number of bytes saved in comments: ' + str(_num_bytes_saved_in_comments) + os.linesep + + ' Number of bytes saved in IDs: ' + str(_num_bytes_saved_in_ids) + os.linesep + + ' Number of bytes saved in lengths: ' + str(_num_bytes_saved_in_lengths) + os.linesep + + ' Number of bytes saved in transformations: ' + str(_num_bytes_saved_in_transforms) + ) def start(options, input, output): From 24c8087bd4c64e5e808abfcb021a33204314869f Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 18 Sep 2016 17:12:33 +0200 Subject: [PATCH 123/270] minor whitespace fix --- scour/scour.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 80923f8..12867da 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1092,23 +1092,22 @@ def createGroupsForCommonAttributes(elem): while curChild >= 0: childNode = elem.childNodes.item(curChild) - if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '' and \ - childNode.nodeName in [ + 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 <g> # 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 + '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', + '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' + # 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) From 47cfb9aa0e2120a5a0c2be995eb04ab8aa3203fa Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 17 Sep 2016 17:11:44 +0200 Subject: [PATCH 124/270] Add unittest for `--strip-xml-space` --- testscour.py | 28 +++++++++++++++++++++++++++- unittests/xml-space.svg | 4 ++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 unittests/xml-space.svg diff --git a/testscour.py b/testscour.py index cd0c59e..9878b01 100755 --- a/testscour.py +++ b/testscour.py @@ -2100,7 +2100,33 @@ class ParseStyleAttribute(unittest.TestCase): 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') + "Style attribute not properly parsed and/or serialized") + + +class StripXmlSpaceAttribute(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/xml-space.svg', + parse_args(['--strip-xml-space'])) + self.assertEqual(doc.documentElement.getAttribute('xml:space'), '', + "'xml:space' attribute not removed from root SVG element" + "when '--strip-xml-space' was specified") + self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', + "'xml:space' attribute removed from a child element" + "when '--strip-xml-space' was specified (should only operate on root SVG element)") + + +class DoNotStripXmlSpaceAttribute(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/xml-space.svg') + self.assertNotEqual(doc.documentElement.getAttribute('xml:space'), '', + "'xml:space' attribute removed from root SVG element" + "when '--strip-xml-space' was NOT specified") + self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', + "'xml:space' attribute removed from a child element" + "when '--strip-xml-space' was NOT specified (should never be removed!)") + # TODO: write tests for --enable-viewboxing # TODO; write a test for embedding rasters diff --git a/unittests/xml-space.svg b/unittests/xml-space.svg new file mode 100644 index 0000000..88a9f50 --- /dev/null +++ b/unittests/xml-space.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="200" height="50" xml:space="preserve"> + <text id="text1" x="5" y="20" xml:space="preserve">Some random text.</text> +</svg> \ No newline at end of file From 829b630d642e20a72ae9a54946015fbc8e276d6b Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 17 Sep 2016 23:40:53 +0200 Subject: [PATCH 125/270] Add unittests which emulate calling the scour module from command line --- testscour.py | 162 ++++++++++++++++++++++++++++++++++++++++-- unittests/minimal.svg | 2 + 2 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 unittests/minimal.svg diff --git a/testscour.py b/testscour.py index 9878b01..da67562 100755 --- a/testscour.py +++ b/testscour.py @@ -20,15 +20,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import +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 unittest import six from six.moves import map, range -from scour.scour import makeWellFormed, parse_args, scourString, scourXmlFile +from scour.scour import makeWellFormed, parse_args, scourString, scourXmlFile, run from scour.svg_regex import svg_parser +from scour import __version__ SVGNS = 'http://www.w3.org/2000/svg' @@ -2112,7 +2116,7 @@ class StripXmlSpaceAttribute(unittest.TestCase): "'xml:space' attribute not removed from root SVG element" "when '--strip-xml-space' was specified") self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', - "'xml:space' attribute removed from a child element" + "'xml:space' attribute removed from a child element " "when '--strip-xml-space' was specified (should only operate on root SVG element)") @@ -2124,10 +2128,160 @@ class DoNotStripXmlSpaceAttribute(unittest.TestCase): "'xml:space' attribute removed from root SVG element" "when '--strip-xml-space' was NOT specified") self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', - "'xml:space' attribute removed from a child element" + "'xml:space' attribute removed from a child element " "when '--strip-xml-space' was NOT specified (should never be removed!)") +class CommandLineUsage(unittest.TestCase): + + USAGE_STRING = "Usage: scour [INPUT.SVG [OUTPUT.SVG]] [OPTIONS]" + MINIMAL_SVG = '<?xml version="1.0" encoding="UTF-8"?>\n' \ + '<svg xmlns="http://www.w3.org/2000/svg"/>\n' + TEMP_SVG_FILE = 'testscour_temp.svg' + + # wrapper function for scour.run() to emulate command line usage + # + # returns an object with the following attributes: + # status: the exit status + # stdout: a string representing the combined output to 'stdout' + # stderr: a string representing the combined output to 'stderr' + def _run_scour(self): + class Result(object): + pass + + result = Result() + try: + run() + result.status = 0 + except SystemExit as exception: # catch any calls to sys.exit() + result.status = exception.code + result.stdout = self.temp_stdout.getvalue() + result.stderr = self.temp_stderr.getvalue() + + return result + + def setUp(self): + # store current values of 'argv', 'stdin', 'stdout' and 'stderr' + self.argv = sys.argv + self.stdin = sys.stdin + self.stdout = sys.stdout + self.stderr = sys.stderr + + # start with a fresh 'argv' + sys.argv = ['scour'] # TODO: Do we need a (more) valid 'argv[0]' for anything? + + # create 'stdin', 'stdout' and 'stderr' with behavior close to the original + # TODO: can we create file objects that behave *exactly* like the original? + # this is a mess since we have to ensure compatibility across Python 2 and 3 and it seems impossible + # to replicate all the details of 'stdin', 'stdout' and 'stderr' + class InOutBuffer(six.StringIO, object): + def write(self, string): + try: + return super(InOutBuffer, self).write(string) + except TypeError: + return super(InOutBuffer, self).write(string.decode()) + + sys.stdin = self.temp_stdin = InOutBuffer() + sys.stdout = self.temp_stdout = InOutBuffer() + sys.stderr = self.temp_stderr = InOutBuffer() + + self.temp_stdin.name = '<stdin>' # Scour wants to print the name of the input file... + + def tearDown(self): + # restore previous values of 'argv', 'stdin', 'stdout' and 'stderr' + sys.argv = self.argv + sys.stdin = self.stdin + sys.stdout = self.stdout + sys.stderr = self.stderr + + # clean up + self.temp_stdin.close() + self.temp_stdout.close() + self.temp_stderr.close() + + def test_no_arguments(self): + # we have to pretend that our input stream is a TTY, otherwise Scour waits for input from stdin + self.temp_stdin.isatty = lambda: True + + result = self._run_scour() + + self.assertEqual(result.status, 2, "Execution of 'scour' without any arguments should exit with status '2'") + self.assertTrue(self.USAGE_STRING in result.stderr, + "Usage information not displayed when calling 'scour' without any arguments") + + def test_version(self): + sys.argv.append('--version') + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Execution of 'scour --version' erorred'") + self.assertEqual(__version__ + "\n", result.stdout, "Unexpected output of 'scour --version'") + + def test_help(self): + sys.argv.append('--help') + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Execution of 'scour --help' erorred'") + self.assertTrue(self.USAGE_STRING in result.stdout and 'Options:' in result.stdout, + "Unexpected output of 'scour --help'") + + def test_stdin_stdout(self): + sys.stdin.write(self.MINIMAL_SVG) + sys.stdin.seek(0) + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Usage of Scour via 'stdin' / 'stdout' erorred'") + self.assertEqual(result.stdout, self.MINIMAL_SVG, "Unexpected SVG output via 'stdout'") + + def test_filein_fileout_named(self): + sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as named parameters errored'") + with open(self.TEMP_SVG_FILE) as file: + file_content = file.read() + self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") + os.remove(self.TEMP_SVG_FILE) + + def test_filein_fileout_positional(self): + sys.argv.extend(['unittests/minimal.svg', self.TEMP_SVG_FILE]) + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as positional parameters errored'") + with open(self.TEMP_SVG_FILE) as file: + file_content = file.read() + self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") + os.remove(self.TEMP_SVG_FILE) + + def test_quiet(self): + sys.argv.append('-q') + sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) + + result = self._run_scour() + os.remove(self.TEMP_SVG_FILE) + + self.assertEqual(result.status, 0, "Execution of 'scour -q ...' erorred'") + self.assertEqual(result.stdout, '', "Output writtent to 'stdout' when '--quiet' options was used") + self.assertEqual(result.stderr, '', "Output writtent to 'stderr' when '--quiet' options was used") + + def test_verbose(self): + sys.argv.append('-v') + sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) + + result = self._run_scour() + os.remove(self.TEMP_SVG_FILE) + + self.assertEqual(result.status, 0, "Execution of 'scour -v ...' erorred'") + self.assertEqual(result.stdout.count('Number'), 14, + "Statistics output not as expected when '--verbose' option was used") + self.assertEqual(result.stdout.count(': 0'), 14, + "Statistics output not as expected when '--verbose' option was used") + + # TODO: write tests for --enable-viewboxing # TODO; write a test for embedding rasters # TODO: write a test for --disable-embed-rasters diff --git a/unittests/minimal.svg b/unittests/minimal.svg new file mode 100644 index 0000000..b9d264c --- /dev/null +++ b/unittests/minimal.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"/> From 45a2869a17ab6e1bae6bcbea2330129bbe3ed6b0 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 18 Sep 2016 18:25:18 +0200 Subject: [PATCH 126/270] Add unittests for `--protect-ids-_` options --- testscour.py | 59 +++++++++++++++++++++++++++++++++++++++ unittests/ids-protect.svg | 8 ++++++ 2 files changed, 67 insertions(+) create mode 100644 unittests/ids-protect.svg diff --git a/testscour.py b/testscour.py index da67562..08eda23 100755 --- a/testscour.py +++ b/testscour.py @@ -389,6 +389,65 @@ class RemoveUnreferencedIDsWhenEnabled(unittest.TestCase): '<svg> ID not stripped') +class ProtectIDs(unittest.TestCase): + + def test_protect_none(self): + doc = scourXmlFile('unittests/ids-protect.svg', + parse_args(['--enable-id-stripping'])) + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', + "ID 'text1' not stripped when none of the '--protect-ids-_' options was specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', + "ID 'text2' not stripped when none of the '--protect-ids-_' options was specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', + "ID 'text3' not stripped when none of the '--protect-ids-_' options was specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', + "ID 'text_custom' not stripped when none of the '--protect-ids-_' options was specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', + "ID 'my_text1' not stripped when none of the '--protect-ids-_' options was specified") + + def test_protect_ids_noninkscape(self): + doc = scourXmlFile('unittests/ids-protect.svg', + parse_args(['--enable-id-stripping', '--protect-ids-noninkscape'])) + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', + "ID 'text1' should have been stripped despite '--protect-ids-noninkscape' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', + "ID 'text2' should have been stripped despite '--protect-ids-noninkscape' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', + "ID 'text3' should have been stripped despite '--protect-ids-noninkscape' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), 'text_custom', + "ID 'text_custom' should NOT have been stripped because of '--protect-ids-noninkscape'") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', + "ID 'my_text1' should have been stripped despite '--protect-ids-noninkscape' being specified") + + def test_protect_ids_list(self): + doc = scourXmlFile('unittests/ids-protect.svg', + parse_args(['--enable-id-stripping', '--protect-ids-list=text2,text3'])) + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', + "ID 'text1' should have been stripped despite '--protect-ids-list' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), 'text2', + "ID 'text2' should NOT have been stripped because of '--protect-ids-list'") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), 'text3', + "ID 'text3' should NOT have been stripped because of '--protect-ids-list'") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', + "ID 'text_custom' should have been stripped despite '--protect-ids-list' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', + "ID 'my_text1' should have been stripped despite '--protect-ids-list' being specified") + + def test_protect_ids_prefix(self): + doc = scourXmlFile('unittests/ids-protect.svg', + parse_args(['--enable-id-stripping', '--protect-ids-prefix=my'])) + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', + "ID 'text1' should have been stripped despite '--protect-ids-prefix' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', + "ID 'text2' should have been stripped despite '--protect-ids-prefix' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', + "ID 'text3' should have been stripped despite '--protect-ids-prefix' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', + "ID 'text_custom' should have been stripped despite '--protect-ids-prefix' being specified") + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), 'my_text1', + "ID 'my_text1' should NOT have been stripped because of '--protect-ids-prefix'") + + class RemoveUselessNestedGroups(unittest.TestCase): def runTest(self): diff --git a/unittests/ids-protect.svg b/unittests/ids-protect.svg new file mode 100644 index 0000000..9809209 --- /dev/null +++ b/unittests/ids-protect.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="110" width="100"> + <text id="text1" x="10" y="20">Text 1</text> + <text id="text2" x="10" y="40">Text 2</text> + <text id="text3" x="10" y="60">Text 3</text> + <text id="text_custom" x="10" y="80">Text custom</text> + <text id="my_text1" x="10" y="100">My text</text> +</svg> From 7e2b5e43df2f5bf6fc724396de73429423b15fe1 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 18 Sep 2016 22:36:21 +0200 Subject: [PATCH 127/270] Add CONTRIBUTING.md with some (hopefully) useful pointers --- CONTRIBUTING.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0b239cc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing + +Contributions to Scour are welcome, feel free to create a pull request! + +In order to be able to merge your PR as fast as possible please try to stick to the following guidelines. + +> _**TL;DR** (if you now what you're doing) – Always run [`make check`](https://github.com/scour-project/scour/blob/master/Makefile) before creating a PR to check for common problems._ + + +## Code Style + +The Scour project tries to follow the coding conventions described in [PEP 8 - The Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/). While there are some inconsistencies in existing code (e.g. with respect to naming conventions and the usage of globals), new code should always abide by the standard. + +To quickly check for common mistakes you can use [`flake8`](https://pypi.python.org/pypi/flake8). Our [Makefile](https://github.com/scour-project/scour/blob/master/Makefile) has a convenience target with the correct options: +```Makefile +make flake8 +``` + +## Unit Tests + +In order to check functionality of Scour and prevent any regressions in existing code a number of tests exist which use the [`unittest`](https://docs.python.org/library/unittest.html) unit testing framework which ships with Python. You can quickly run the tests by using the [Makefile](https://github.com/scour-project/scour/blob/master/Makefile) convenience target: +```Makefile +make test +``` + +These tests are run automatically on all PRs using [TravisCI](https://travis-ci.org/scour-project/scour) and have to pass at all times! When you add new functionality you should always include suitable tests with your PR (see [`testscour.py`](https://github.com/scour-project/scour/blob/master/testscour.py)). + +### Coverage + +To ensure that all possible code conditions are covered by a test you can use [`coverage`](https://pypi.python.org/pypi/coverage). The [Makefile](https://github.com/scour-project/scour/blob/master/Makefile) convenience target automatically creates an HTML report in `htmlcov/index.html`: +```Makefile +make coverage +``` + +These reports are also created automatically by our TravisCI builds and are accessible via [Codecov](https://codecov.io/gh/scour-project/scour) \ No newline at end of file From 8d20805976624a482759198b20ecd9167ef90613 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 18 Sep 2016 22:41:24 +0200 Subject: [PATCH 128/270] Add three images (for usage with a future unittest) --- unittests/raster.gif | Bin 0 -> 41 bytes unittests/raster.jpg | Bin 0 -> 350 bytes unittests/raster.png | Bin 0 -> 88 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 unittests/raster.gif create mode 100644 unittests/raster.jpg create mode 100644 unittests/raster.png diff --git a/unittests/raster.gif b/unittests/raster.gif new file mode 100644 index 0000000000000000000000000000000000000000..6ad1d037322e38e9aae35669f510a2f01ece3ec0 GIT binary patch literal 41 pcmZ?wbh9u|WM*JwSjf!4!0`V+hyYUmb$|>8kT?SqQ%?YcH2~Ma36uZ; literal 0 HcmV?d00001 diff --git a/unittests/raster.jpg b/unittests/raster.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2a3c4bfb20723ed38252f2458a930c32ea95f55 GIT binary patch literal 350 zcmex=<NpH&0WUXCHwH#VMg|WcWcdG&LD;z{HL)Z$MWH;iBtya7(>LJ%Z3btM5{dxG z5Q+={Y5sqJL6C!ik%5_+QILU2kdaxC@&6G95ugYv-~x(D2{16SqDXKsu(F$%?*Ph) z0?k6z!4A?QBgnwW%!(|*!N9(Q)55Z>;{PoM9%e?Moy>v^_6&y4TK~PB)j#K}*sAIU c_q318E}1`_f0t;Lp3kv&i#H|gUH$(i02P%sPyhe` literal 0 HcmV?d00001 diff --git a/unittests/raster.png b/unittests/raster.png new file mode 100644 index 0000000000000000000000000000000000000000..81b33f6e486dd5205b047b1ebea3068749e1cbe6 GIT binary patch literal 88 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y@1R~S={-^^f&H$efR|bav|AFlP3~5`x`T-eS go-U3d9M_XQfb1lOL}{_a4L}xyr>mdKI;Vst0P1EFL;wH) literal 0 HcmV?d00001 From 462460a5125c1d617dc9e68a8cae8ccd6fb4f7d3 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Fri, 23 Sep 2016 00:20:14 +0200 Subject: [PATCH 129/270] Fix `embedRasters()` function. It was not Python 3 compatible and usually would not have worked with local files. --- scour/scour.py | 75 +++++++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 12867da..2ae8230 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -60,7 +60,7 @@ from collections import namedtuple from decimal import Context, Decimal, InvalidOperation, getcontext import six -from six.moves import range +from six.moves import range, urllib from scour.svg_regex import svg_parser from scour.svg_transform import svg_transform_parser @@ -2939,47 +2939,65 @@ def removeComments(element): def embedRasters(element, options): import base64 - import urllib """ Converts raster references to inline images. NOTE: there are size limits to base64-encoding handling in browsers - """ + """ global _num_rasters_embedded 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:] - # look for 'png', 'jpg', and 'gif' extensions - if ext == 'png' or ext == 'jpg' or ext == 'gif': + # only operate on files with 'png', 'jpg', and 'gif' file extensions + if ext in ['png', 'jpg', 'gif']: + # fix common issues with file paths + # TODO: should we warn the user instead of trying to correct those invalid URIs? + # convert backslashes to slashes + href_fixed = href.replace('\\', '/') + # absolute 'file:' URIs have to use three slashes (unless specifying a host which I've never seen) + href_fixed = re.sub('file:/+', 'file:///', href_fixed) - # 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 = '.' + # parse the URI to get scheme and path + # in principle it would make sense to work only with this ParseResult and call 'urlunparse()' in the end + # however 'urlunparse(urlparse(file:raster.png))' -> 'file:///raster.png' which is nonsense + parsed_href = urllib.parse.urlparse(href_fixed) + + # assume locations without protocol point to local files (and should use the 'file:' protocol) + if parsed_href.scheme == '': + parsed_href = parsed_href._replace(scheme='file') + if href_fixed[0] == '/': + href_fixed = 'file://' + href_fixed + else: + href_fixed = 'file:' + href_fixed + + # relative local paths are relative to the input file, therefore temporarily change the working dir + working_dir_old = None + if parsed_href.scheme == 'file' and parsed_href.path[0] != '/': if options.infilename: - infilename = options.infilename - href = os.path.join(os.path.dirname(infilename), href) + working_dir_old = os.getcwd() + working_dir_new = os.path.abspath(os.path.dirname(options.infilename)) + os.chdir(working_dir_new) - 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() + # open/download the file + try: + file = urllib.request.urlopen(href_fixed) + rasterdata = file.read() + file.close() + except Exception as e: + print("WARNING: Could not open file '" + href + "' for embedding. " + "The raster image will be kept as a reference but might be invalid. " + "(Exception details: " + str(e) + ")", file=sys.stderr) + rasterdata = '' + finally: + # always restore initial working directory if we changed it above + if working_dir_old is not None: + os.chdir(working_dir_old) - # ... should we remove all images which don't resolve? + # TODO: should we remove all images which don't resolve? + # then we also have to consider unreachable remote locations (i.e. if there is no internet connection) if rasterdata != '': # base64-encode raster b64eRaster = base64.b64encode(rasterdata) @@ -2991,7 +3009,8 @@ def embedRasters(element, options): if ext == 'jpg': ext = 'jpeg' - element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster) + element.setAttributeNS(NS['XLINK'], 'href', + 'data:image/' + ext + ';base64,' + b64eRaster.decode()) _num_rasters_embedded += 1 del b64eRaster From 8cc97601c45d9efaa22c11e5961b3883132e312e Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Fri, 23 Sep 2016 22:32:32 +0200 Subject: [PATCH 130/270] scourXmlFile(): Set specified 'filename' as input filename so relative references will work --- scour/scour.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index 2ae8230..e91d385 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3519,10 +3519,17 @@ def scourString(in_string, options=None): # input is a filename # returns the minidom doc representation of the SVG def scourXmlFile(filename, options=None): + # we need to set infilename (otherwise relative references in the SVG won't work) + if options is None: + options = generateDefaultOptions() + options.infilename = filename + + # open the file and scour it with open(filename, "rb") as f: in_string = f.read() out_string = scourString(in_string, options) + # prepare the output xml.dom.minidom object doc = xml.dom.minidom.parseString(out_string.encode('utf-8')) # since minidom does not seem to parse DTDs properly From 902e112a96d1f522ec5d82d55a0cd62db3790eae Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Fri, 23 Sep 2016 22:33:01 +0200 Subject: [PATCH 131/270] Add unittests for embedding rasters (and --disable-embed-rasters) --- testscour.py | 57 +++++++++++++++++++++++++++++-- unittests/raster-formats.svg | 7 ++++ unittests/raster-paths-local.svg | 19 +++++++++++ unittests/raster-paths-remote.svg | 8 +++++ 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 unittests/raster-formats.svg create mode 100644 unittests/raster-paths-local.svg create mode 100644 unittests/raster-paths-remote.svg diff --git a/testscour.py b/testscour.py index 08eda23..f14e901 100755 --- a/testscour.py +++ b/testscour.py @@ -2341,9 +2341,62 @@ class CommandLineUsage(unittest.TestCase): "Statistics output not as expected when '--verbose' option was used") +class EmbedRasters(unittest.TestCase): + + # quick way to ping a host using the OS 'ping' command and return the execution result + def _ping(host): + import os + import platform + + system = platform.system().lower() + ping_count = '-n' if system == 'windows' else '-c' + dev_null = 'NUL' if system == 'windows' else '/dev/null' + + return os.system('ping ' + ping_count + ' 1 ' + host + ' > ' + dev_null) + + def test_disable_embed_rasters(self): + doc = scourXmlFile('unittests/raster-formats.svg', + parse_args(['--disable-embed-rasters'])) + self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), 'raster.png', + "Raster image embedded when '--disable-embed-rasters' was specified") + + def test_raster_formats(self): + doc = scourXmlFile('unittests/raster-formats.svg') + self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAABAgMAAABmjvwnAAAAC' + 'VBMVEUAAP//AAAA/wBmtfVOAAAACklEQVQI12NIAAAAYgBhGxZhsAAAAABJRU5ErkJggg==', + "Raster image (PNG) not correctly embedded.") + self.assertEqual(doc.getElementById('gif').getAttribute('xlink:href'), + 'data:image/gif;base64,R0lGODdhAwABAKEDAAAA//8AAAD/AP///ywAAAAAAwABAAACAoxQADs=', + "Raster image (GIF) not correctly embedded.") + self.assertEqual(doc.getElementById('jpg').getAttribute('xlink:href'), + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/' + '2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' + '2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' + 'wAARCAABAAMDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABoQAAEFAQAAAAAAAAAAAAAAAAgABQc3d7j/' + 'xAAVAQEBAAAAAAAAAAAAAAAAAAAHCv/EABwRAAEDBQAAAAAAAAAAAAAAAAgAB7gJODl2eP/aAAwDAQACEQMRAD8AMeaF' + '/u2aj5z1Fqp7oN4rxx2kn5cPuhV6LkzG7qOyYL2r/9k=', + "Raster image (JPG) not correctly embedded.") + + def test_raster_paths_local(self): + doc = scourXmlFile('unittests/raster-paths-local.svg') + images = doc.getElementsByTagName('image') + for image in images: + href = image.getAttribute('xlink:href') + self.assertTrue(href.startswith('data:image/'), + "Raster image from local path '" + href + "' not embedded.") + + @unittest.skipIf(_ping('raw.githubusercontent.com') != 0, "Remote server not reachable.") + def test_raster_paths_remote(self): + doc = scourXmlFile('unittests/raster-paths-remote.svg') + images = doc.getElementsByTagName('image') + for image in images: + href = image.getAttribute('xlink:href') + self.assertTrue(href.startswith('data:image/'), + "Raster image from remote path '" + href + "' not embedded.") + + # TODO: write tests for --enable-viewboxing -# TODO; write a test for embedding rasters -# TODO: write a test for --disable-embed-rasters # TODO: write tests for --keep-editor-data if __name__ == '__main__': diff --git a/unittests/raster-formats.svg b/unittests/raster-formats.svg new file mode 100644 index 0000000..c31b65a --- /dev/null +++ b/unittests/raster-formats.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink" width="170" height="210"> + <text x="10" y="20">Three different formats</text> + <image id="png" x="10" y="30" width="150" height="50" xlink:href="raster.png"/> + <image id="gif" x="10" y="90" width="150" height="50" xlink:href="raster.gif"/> + <image id="jpg" x="10" y="150" width="150" height="50" xlink:href="raster.jpg"/> +</svg> \ No newline at end of file diff --git a/unittests/raster-paths-local.svg b/unittests/raster-paths-local.svg new file mode 100644 index 0000000..9cc6ed9 --- /dev/null +++ b/unittests/raster-paths-local.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink" width="330" height="270"> + <g> + <text x="10" y="20">Local files</text> + <image x="10" y="30" width="150" height="50" xlink:href="raster.png"/> + <image x="10" y="90" width="150" height="50" xlink:href="./raster.png"/> + <image x="10" y="150" width="150" height="50" xlink:href="../unittests/raster.png"/> + <!-- path can also be absolute but this will obviously not work across systems --> + <image x="10" y="210" width="150" height="50" xlink:href="/E:/Temp/Scour/scour.git/unittests/raster.png"/> + </g> + <g transform="translate(160)"> + <text x="10" y="20">Local files (file: protocol)</text> + <image x="10" y="30" width="150" height="50" xlink:href="file:raster.png"/> + <image x="10" y="90" width="150" height="50" xlink:href="file:./raster.png"/> + <image x="10" y="150" width="150" height="50" xlink:href="file:../unittests/raster.png"/> + <!-- path can also be absolute but this will obviously not work across systems --> + <image x="10" y="210" width="150" height="50" xlink:href="file:///E:/Temp/Scour/scour.git/unittests/raster.png"/> + </g> +</svg> \ No newline at end of file diff --git a/unittests/raster-paths-remote.svg b/unittests/raster-paths-remote.svg new file mode 100644 index 0000000..ede7783 --- /dev/null +++ b/unittests/raster-paths-remote.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink" width="170" height="270"> + <g> + <text x="10" y="20">Files from internet</text> + <image x="10" y="30" width="150" height="50" xlink:href="http://raw.githubusercontent.com/scour-project/scour/master/unittests/raster.png"/> + <image x="10" y="90" width="150" height="50" xlink:href="https://raw.githubusercontent.com/scour-project/scour/master/unittests/raster.png"/> + </g> +</svg> \ No newline at end of file From de1441fd58294585cc0e3ad296572960d0b2aac5 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Fri, 23 Sep 2016 23:16:19 +0200 Subject: [PATCH 132/270] Exclude (system specific) absolute paths from test file and add a unittest that creates/tests absolute paths on-the-fly --- testscour.py | 23 +++++++++++++++++++++++ unittests/raster-paths-local.svg | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/testscour.py b/testscour.py index f14e901..6098914 100755 --- a/testscour.py +++ b/testscour.py @@ -2386,6 +2386,29 @@ class EmbedRasters(unittest.TestCase): self.assertTrue(href.startswith('data:image/'), "Raster image from local path '" + href + "' not embedded.") + def test_raster_paths_local_absolute(self): + with open('unittests/raster-formats.svg', 'r') as f: + svg = f.read() + + # create a reference string by scouring the original file with relative links + options = ScourOptions + options.infilename = 'unittests/raster-formats.svg' + reference_svg = scourString(svg, options) + + # this will not always create formally valid paths but it'll check how robust our implementation is + # (the third path is invalid for sure because file: needs three slashes according to URI spec) + svg = svg.replace('raster.png', + '/' + os.path.abspath(os.path.dirname(__file__)) + '\\unittests\\raster.png') + svg = svg.replace('raster.gif', + 'file:///' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.gif') + svg = svg.replace('raster.jpg', + 'file:/' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.jpg') + + svg = scourString(svg) + + self.assertEqual(svg, reference_svg, + "Raster images from absolute local paths not properly embedded.") + @unittest.skipIf(_ping('raw.githubusercontent.com') != 0, "Remote server not reachable.") def test_raster_paths_remote(self): doc = scourXmlFile('unittests/raster-paths-remote.svg') diff --git a/unittests/raster-paths-local.svg b/unittests/raster-paths-local.svg index 9cc6ed9..61db8ab 100644 --- a/unittests/raster-paths-local.svg +++ b/unittests/raster-paths-local.svg @@ -6,7 +6,7 @@ <image x="10" y="90" width="150" height="50" xlink:href="./raster.png"/> <image x="10" y="150" width="150" height="50" xlink:href="../unittests/raster.png"/> <!-- path can also be absolute but this will obviously not work across systems --> - <image x="10" y="210" width="150" height="50" xlink:href="/E:/Temp/Scour/scour.git/unittests/raster.png"/> + <!--<image x="10" y="210" width="150" height="50" xlink:href="/E:/Temp/Scour/scour.git/unittests/raster.png"/>--> </g> <g transform="translate(160)"> <text x="10" y="20">Local files (file: protocol)</text> @@ -14,6 +14,6 @@ <image x="10" y="90" width="150" height="50" xlink:href="file:./raster.png"/> <image x="10" y="150" width="150" height="50" xlink:href="file:../unittests/raster.png"/> <!-- path can also be absolute but this will obviously not work across systems --> - <image x="10" y="210" width="150" height="50" xlink:href="file:///E:/Temp/Scour/scour.git/unittests/raster.png"/> + <!--<image x="10" y="210" width="150" height="50" xlink:href="file:///E:/Temp/Scour/scour.git/unittests/raster.png"/>--> </g> </svg> \ No newline at end of file From 2fe7152a1e54804e8f52a9e2d7a01abec54a3d55 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 25 Sep 2016 15:34:28 +0200 Subject: [PATCH 133/270] Fix logic from 4a5b924d375461444c4fe2f61942a3febf37af1d (which was still wrong after 4410f91dad3a780f52da75bff0103832d7ed7f2b) --- scour/scour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index e91d385..00d8c7b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3794,9 +3794,9 @@ def start(options, input, output): output.write(out_string) # Close input and output files (but do not attempt to close stdin/stdout!) - if not ((input is sys.stdin) or (hasattr(input, 'buffer') and input is sys.stdin.buffer)): + if not ((input is sys.stdin) or (hasattr(sys.stdin, 'buffer') and input is sys.stdin.buffer)): input.close() - if not ((output is sys.stdout) or (hasattr(output, 'buffer') and output is sys.stdout.buffer)): + if not ((output is sys.stdout) or (hasattr(sys.stdout, 'buffer') and output is sys.stdout.buffer)): output.close() end = walltime() From 285d73e5a6bcd0dbc8a7353f704585eff3fdc9a8 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 25 Sep 2016 15:44:07 +0200 Subject: [PATCH 134/270] Fix statistics out put for "Number of comments removed" --- scour/scour.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 00d8c7b..7a93fb8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2928,13 +2928,17 @@ def removeComments(element): Removes comments from the element and its children. """ global _num_bytes_saved_in_comments + num = 0 if isinstance(element, xml.dom.minidom.Comment): _num_bytes_saved_in_comments += len(element.data) element.parentNode.removeChild(element) + num += 1 else: for subelement in element.childNodes[:]: - removeComments(subelement) + num += removeComments(subelement) + + return num def embedRasters(element, options): From 49cb54268917a06453a86c7278fedceb158611a5 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 25 Sep 2016 18:49:05 +0200 Subject: [PATCH 135/270] Make `sanitizeOptions()` work with an empty parameter list --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 7a93fb8..e26e09d 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3723,7 +3723,7 @@ def generateDefaultOptions(): # sanitizes options by updating attributes in a set of defaults options while discarding unknown attributes -def sanitizeOptions(options): +def sanitizeOptions(options=None): optionsDict = dict((key, getattr(options, key)) for key in dir(options) if not key.startswith('__')) sanitizedOptions = _options_parser.get_default_values() From d9b369864d04387b366caa9769ffb198e644e503 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 25 Sep 2016 19:02:19 +0200 Subject: [PATCH 136/270] Reimplement `generateDefaultOptions()` by simply calling `sanitizeOptions()` --- scour/scour.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index e26e09d..23be0c5 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3710,16 +3710,10 @@ def parse_args(args=None, ignore_additional_args=False): return options +# this function was replaced by 'sanitizeOptions()' and is only kept for backwards compatibility +# TODO: delete this at some point or continue to keep it around? def generateDefaultOptions(): - # FIXME: clean up this mess/hack and refactor arg parsing to argparse - class Struct: - - def __init__(self, **entries): - self.__dict__.update(entries) - - d = parse_args(args=[], ignore_additional_args=True).__dict__.copy() - - return Struct(**d) + return sanitizeOptions() # sanitizes options by updating attributes in a set of defaults options while discarding unknown attributes From c45f050fe6680a64415c245a8ecf7fc7ae09f026 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Tue, 25 Oct 2016 23:16:47 +0200 Subject: [PATCH 137/270] Update HISTORY.md (0.35 has been released) --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 8f8a51e..ae58729 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,6 @@ # Release Notes for Scour -## Version 0.35 (not released yet) +## Version 0.35 (2016-09-14) * Drop official support for Python 2.6. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 2.6 and should find/fix any compatibility issues pull requests are welcome, though.) * Fix: Unused IDs were not shortended when `--shorten-ids` was used. ([#19](https://github.com/scour-project/scour/issues/62)) From 6cf8c2b7d92848f9eddc0d5badf8f1fd75cd178a Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 27 Nov 2016 18:38:29 +0100 Subject: [PATCH 138/270] call `sanitizeOptions()` in `start()` to prevent a third-party breakage --- scour/scour.py | 10 ++++++---- testscour.py | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 23be0c5..a1b99cf 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3523,10 +3523,10 @@ def scourString(in_string, options=None): # input is a filename # returns the minidom doc representation of the SVG def scourXmlFile(filename, options=None): - # we need to set infilename (otherwise relative references in the SVG won't work) - if options is None: - options = generateDefaultOptions() - options.infilename = filename + # sanitize options (take missing attributes from defaults, discard unknown attributes) + options = sanitizeOptions(options) + # we need to make sure infilename is set correctly (otherwise relative references in the SVG won't work) + options.ensure_value("infilename", filename) # open the file and scour it with open(filename, "rb") as f: @@ -3783,6 +3783,8 @@ def getReport(): def start(options, input, output): + # sanitize options (take missing attributes from defaults, discard unknown attributes) + options = sanitizeOptions(options) start = walltime() diff --git a/testscour.py b/testscour.py index 6098914..560f79b 100755 --- a/testscour.py +++ b/testscour.py @@ -30,7 +30,7 @@ import unittest import six from six.moves import map, range -from scour.scour import makeWellFormed, parse_args, scourString, scourXmlFile, run +from scour.scour import makeWellFormed, parse_args, scourString, scourXmlFile, start, run from scour.svg_regex import svg_parser from scour import __version__ @@ -58,15 +58,45 @@ class ScourOptions: class EmptyOptions(unittest.TestCase): - def runTest(self): + MINIMAL_SVG = '<?xml version="1.0" encoding="UTF-8"?>\n' \ + '<svg xmlns="http://www.w3.org/2000/svg"/>\n' + + def test_scourString(self): options = ScourOptions try: - scourXmlFile('unittests/ids-to-strip.svg', options) + scourString(self.MINIMAL_SVG, options) fail = False except: fail = True self.assertEqual(fail, False, - 'Exception when calling Scour with empty options object') + 'Exception when calling "scourString" with empty options object') + + def test_scourXmlFile(self): + options = ScourOptions + try: + scourXmlFile('unittests/minimal.svg', options) + fail = False + except: + fail = True + self.assertEqual(fail, False, + 'Exception when calling "scourXmlFile" with empty options object') + + def test_start(self): + options = ScourOptions + input = open('unittests/minimal.svg', 'rb') + output = open('testscour_temp.svg', 'wb') + + stdout_temp = sys.stdout + sys.stdout = None + try: + start(options, input, output) + fail = False + except: + fail = True + sys.stdout = stdout_temp + + self.assertEqual(fail, False, + 'Exception when calling "start" with empty options object') class InvalidOptions(unittest.TestCase): From 25549b35d30c3c4d96c4cf817dff0c242bc362ba Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 27 Nov 2016 18:47:17 +0100 Subject: [PATCH 139/270] Some whitespace fixes to make newer versions of flake8 happy --- scour/scour.py | 2 ++ scour/svg_regex.py | 3 +++ scour/svg_transform.py | 3 +++ testcss.py | 1 + 4 files changed, 9 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index a1b99cf..a6dede0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -405,6 +405,7 @@ default_properties = { # excluded all properties with 'auto' as default 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+)") @@ -1945,6 +1946,7 @@ def removeDefaultAttributeValues(node, options, tainted=set()): 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*") diff --git a/scour/svg_regex.py b/scour/svg_regex.py index 0926363..d4dfe3a 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -55,6 +55,8 @@ class _EOF(object): def __repr__(self): return 'EOF' + + EOF = _EOF() lexicon = [ @@ -97,6 +99,7 @@ class Lexer(object): break yield (EOF, None) + svg_lexer = Lexer(lexicon) diff --git a/scour/svg_transform.py b/scour/svg_transform.py index 944d34c..83454b3 100644 --- a/scour/svg_transform.py +++ b/scour/svg_transform.py @@ -70,6 +70,8 @@ class _EOF(object): def __repr__(self): return 'EOF' + + EOF = _EOF() lexicon = [ @@ -114,6 +116,7 @@ class Lexer(object): break yield (EOF, None) + svg_lexer = Lexer(lexicon) diff --git a/testcss.py b/testcss.py index a342b5e..d7fd3e2 100755 --- a/testcss.py +++ b/testcss.py @@ -52,5 +52,6 @@ class ElementSelectorWithProperty(unittest.TestCase): 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() From 210c5f64abdf336e25c58ca927093b3aa94c0a0c Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 27 Nov 2016 19:10:29 +0100 Subject: [PATCH 140/270] Update HISTORY.md --- HISTORY.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index ae58729..20a733d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Release Notes for Scour +## Version 0.36 (not released yet) +* Fix embedding of raster images which was broken in most cases and did not work at all in Python 3. ([#120](https://github.com/scour-project/scour/issues/62)) +* Some minor fixes for statistics output. + + ## Version 0.35 (2016-09-14) * Drop official support for Python 2.6. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 2.6 and should find/fix any compatibility issues pull requests are welcome, though.) From b00b374e648cb95a4fb0abd8f593499907444598 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 18 Feb 2017 18:06:09 +0100 Subject: [PATCH 141/270] Fix generation of non-scientific number representation. Before numbers often were already in scientific notation due to the str() implementation of Decimal leading to strange optimization results. --- scour/scour.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index a6dede0..24bfaa6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2634,10 +2634,8 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of 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") + # Gather the non-scientific notation version of the coordinate. + nonsci = '{0:f}'.format(length) if not needsRendererWorkaround: if len(nonsci) > 2 and nonsci[:2] == '0.': nonsci = nonsci[1:] # remove the 0, leave the dot From 8f87118725a70329801b3f42b8eed2cc8c8e0dc1 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 18 Feb 2017 19:01:26 +0100 Subject: [PATCH 142/270] Only use number representation with reduced precision if it is shorter than the initial representation. Before it could happen that "123" was replaced with "1e3" if precision was set to 1 which is obviously not desirable. --- scour/scour.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 24bfaa6..7d2e320 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2623,6 +2623,16 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of if not isinstance(length, Decimal): length = getcontext().create_decimal(str(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() + + # Gather the initial non-scientific notation version of the coordinate (we want to compare with it later) + initial_value = '{0:f}'.format(length) + # reduce numeric precision # plus() corresponds to the unary prefix plus operator and applies context precision and rounding length = scouringContext.plus(length) @@ -2641,6 +2651,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of 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 + return_value = nonsci # 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). @@ -2653,11 +2664,13 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of sci = six.text_type(length) + 'e' + six.text_type(exponent) if len(sci) < len(nonsci): - return sci - else: - return nonsci + return_value = sci + + # Return the shortest representation (if they are equal prefer the original as it still has the full precision) + if len(return_value) < len(initial_value): + return return_value else: - return nonsci + return initial_value def reducePrecision(element): From a69efb3a558b91603facaf18912403b658ad1451 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 18 Feb 2017 19:36:19 +0100 Subject: [PATCH 143/270] Add unittests for b00b374e648cb95a4fb0abd8f593499907444598 and 8f87118725a70329801b3f42b8eed2cc8c8e0dc1 --- testscour.py | 34 ++++++++++++++++++++++++++++++++++ unittests/path-precision.svg | 11 ++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/testscour.py b/testscour.py index 560f79b..879b7e8 100755 --- a/testscour.py +++ b/testscour.py @@ -956,6 +956,40 @@ class LimitPrecisionInPathData(unittest.TestCase): 'Not correctly limiting precision on path data') +class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=1'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 12 123 1e3 1e4 1e5", + 'Precision not correctly reduced with "--set-precision=1" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1e3 -1e4 -1e5", + 'Precision not correctly reduced with "--set-precision=1" ' + 'for path with ID ' + paths[4].getAttribute('id')) + + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=2'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 12 123 1234 12345 1.2e5", + 'Precision not correctly reduced with "--set-precision=2" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-1.2e5", + 'Precision not correctly reduced with "--set-precision=2" ' + 'for path with ID ' + paths[4].getAttribute('id')) + + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=3'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 12 123 1234 12345 123456", + 'Precision not correctly reduced with "--set-precision=3" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-123456", + 'Precision not correctly reduced with "--set-precision=3" ' + 'for path with ID ' + paths[4].getAttribute('id')) + + class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): def runTest(self): diff --git a/unittests/path-precision.svg b/unittests/path-precision.svg index 8e1e267..1d644e4 100644 --- a/unittests/path-precision.svg +++ b/unittests/path-precision.svg @@ -1,4 +1,9 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" version="1.1"> - <path d="M 100.0000001 99.9999999 h100.01 v123456789.123456789 h-100 z" fill="red" /> +<?xml version="1.0" encoding="UTF-8"?> +<svg vxmlns="http://www.w3.org/2000/svg"> + <path id="p0" d="M 100.0000001 99.9999999 h100.01 v123456789.123456789 h-100 z" /> + + <path id="p1" d="m 1 12 123 1234 12345 123456 " /> + <path id="p2" d="m 1.0 12.0 123.0 1234.0 12345.0 123456.0" /> + <path id="p3" d="m 01 012 0123 01234 012345 0123456 " /> + <path id="p4" d="m -1 -12 -123 -1234 -12345 -123456 " /> </svg> From f5a61eeeb338932191a57264d96527c5d6fbb52e Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 19 Feb 2017 00:39:36 +0100 Subject: [PATCH 144/270] Even better fix for 8f87118725a70329801b3f42b8eed2cc8c8e0dc1 (previous solution still did not work for numbers like 123.4 with precision < 3) --- scour/scour.py | 20 +++++--------------- testscour.py | 22 ++++++++++++++++++++++ unittests/path-precision.svg | 2 ++ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 7d2e320..b4e848b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2622,16 +2622,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of """ if not isinstance(length, Decimal): length = getcontext().create_decimal(str(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() - - # Gather the initial non-scientific notation version of the coordinate (we want to compare with it later) - initial_value = '{0:f}'.format(length) + initial_length = length # reduce numeric precision # plus() corresponds to the unary prefix plus operator and applies context precision and rounding @@ -2645,7 +2636,10 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of length = length.normalize() # Gather the non-scientific notation version of the coordinate. + # Re-quantize from the initial value to prevent unnecessary loss of precision + # (e.g. 123.4 should become 123, not 120 or even 100) nonsci = '{0:f}'.format(length) + nonsci = '{0:f}'.format(initial_length.quantize(Decimal(nonsci))) if not needsRendererWorkaround: if len(nonsci) > 2 and nonsci[:2] == '0.': nonsci = nonsci[1:] # remove the 0, leave the dot @@ -2666,11 +2660,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of if len(sci) < len(nonsci): return_value = sci - # Return the shortest representation (if they are equal prefer the original as it still has the full precision) - if len(return_value) < len(initial_value): - return return_value - else: - return initial_value + return return_value def reducePrecision(element): diff --git a/testscour.py b/testscour.py index 879b7e8..43eeb03 100755 --- a/testscour.py +++ b/testscour.py @@ -968,6 +968,9 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1e3 -1e4 -1e5", 'Precision not correctly reduced with "--set-precision=1" ' 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", + 'Precision not correctly reduced with "--set-precision=1" ' + 'for path with ID ' + paths[5].getAttribute('id')) doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=2'])) paths = doc.getElementsByTagNameNS(SVGNS, 'path') @@ -978,6 +981,9 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-1.2e5", 'Precision not correctly reduced with "--set-precision=2" ' 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", + 'Precision not correctly reduced with "--set-precision=2" ' + 'for path with ID ' + paths[5].getAttribute('id')) doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=3'])) paths = doc.getElementsByTagNameNS(SVGNS, 'path') @@ -988,6 +994,22 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-123456", 'Precision not correctly reduced with "--set-precision=3" ' 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", + 'Precision not correctly reduced with "--set-precision=3" ' + 'for path with ID ' + paths[5].getAttribute('id')) + + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=4'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 12 123 1234 12345 123456", + 'Precision not correctly reduced with "--set-precision=4" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-123456", + 'Precision not correctly reduced with "--set-precision=4" ' + 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123.5 101-123.5-101", + 'Precision not correctly reduced with "--set-precision=4" ' + 'for path with ID ' + paths[5].getAttribute('id')) class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): diff --git a/unittests/path-precision.svg b/unittests/path-precision.svg index 1d644e4..e075ec7 100644 --- a/unittests/path-precision.svg +++ b/unittests/path-precision.svg @@ -6,4 +6,6 @@ <path id="p2" d="m 1.0 12.0 123.0 1234.0 12345.0 123456.0" /> <path id="p3" d="m 01 012 0123 01234 012345 0123456 " /> <path id="p4" d="m -1 -12 -123 -1234 -12345 -123456 " /> + + <path id="p6" d="m 123.456 101.001 -123.456 -101.001" /> </svg> From 3e4c8d793fd5d3eb3f0323a1e56d16c3d5ad9bf5 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 19 Feb 2017 15:19:53 +0100 Subject: [PATCH 145/270] Typo in unittest svg file --- unittests/path-precision.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittests/path-precision.svg b/unittests/path-precision.svg index e075ec7..9f2bc38 100644 --- a/unittests/path-precision.svg +++ b/unittests/path-precision.svg @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<svg vxmlns="http://www.w3.org/2000/svg"> +<svg xmlns="http://www.w3.org/2000/svg"> <path id="p0" d="M 100.0000001 99.9999999 h100.01 v123456789.123456789 h-100 z" /> <path id="p1" d="m 1 12 123 1234 12345 123456 " /> From 01cb120d718d200e28cbde7e28adbf85eaa6831b Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 19 Feb 2017 15:22:50 +0100 Subject: [PATCH 146/270] Reduce precision of lengths in viewBox This fixes #127. Also simplify splitting of viewBox lengths and avoiding a "FutureWarning: split() requires a non-empty pattern match" at the same time --- scour/scour.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index b4e848b..d3a3d71 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3037,7 +3037,7 @@ def properlySizeDoc(docElement, options): # 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) + vbSep = re.split('[, ]+', docElement.getAttribute('viewBox')) # if we have a valid viewBox we need to check it vbWidth, vbHeight = 0, 0 if len(vbSep) == 4: @@ -3471,6 +3471,11 @@ def scourString(in_string, options=None): 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: if elem.getAttribute(attr) != '': elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) + viewBox = doc.documentElement.getAttribute('viewBox') + if viewBox: + lengths = re.split('[, ]+', viewBox) + lengths = [scourUnitlessLength(lenght) for lenght in lengths] + doc.documentElement.setAttribute('viewBox', ' '.join(lengths)) # more length scouring in this function _num_bytes_saved_in_lengths = reducePrecision(doc.documentElement) From 0ffefcd8bb6ad02aefff1063e0a7a859cad75893 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 19 Feb 2017 15:39:53 +0100 Subject: [PATCH 147/270] Unittests for `--enable-viewboxing` --- testscour.py | 16 +++++++++++++++- unittests/viewbox-create.svg | 3 +++ unittests/viewbox-remove.svg | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 unittests/viewbox-create.svg create mode 100644 unittests/viewbox-remove.svg diff --git a/testscour.py b/testscour.py index 43eeb03..6daec27 100755 --- a/testscour.py +++ b/testscour.py @@ -2505,7 +2505,21 @@ class EmbedRasters(unittest.TestCase): "Raster image from remote path '" + href + "' not embedded.") -# TODO: write tests for --enable-viewboxing +class ViewBox(unittest.TestCase): + + def test_viewbox_create(self): + doc = scourXmlFile('unittests/viewbox-create.svg', parse_args(['--enable-viewboxing'])) + viewBox = doc.documentElement.getAttribute('viewBox') + self.assertEqual(viewBox, '0 0 123.46 654.32', "viewBox not properly created with '--enable-viewboxing'.") + + def test_viewbox_remove_width_and_height(self): + doc = scourXmlFile('unittests/viewbox-remove.svg', parse_args(['--enable-viewboxing'])) + width = doc.documentElement.getAttribute('width') + height = doc.documentElement.getAttribute('height') + self.assertEqual(width, '', "width not removed with '--enable-viewboxing'.") + self.assertEqual(height, '', "height not removed with '--enable-viewboxing'.") + + # TODO: write tests for --keep-editor-data if __name__ == '__main__': diff --git a/unittests/viewbox-create.svg b/unittests/viewbox-create.svg new file mode 100644 index 0000000..0d250db --- /dev/null +++ b/unittests/viewbox-create.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="123.456" height="654.321"> +</svg> diff --git a/unittests/viewbox-remove.svg b/unittests/viewbox-remove.svg new file mode 100644 index 0000000..8fa8307 --- /dev/null +++ b/unittests/viewbox-remove.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="123.456" height="654.321" viewBox="0 0 123.456 654.321"> +</svg> From 3b41d3a5470f43149c590425442f165870bb58c1 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 19 Feb 2017 18:04:36 +0100 Subject: [PATCH 148/270] Add Python 3.6 to tests and simplify .travis.yml by using 'tox-travis' --- .travis.yml | 26 ++++++++++---------------- tox.ini | 1 + 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48a15d5..a853a19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,31 +1,25 @@ language: python +python: + - pypy + - 2.7 + - 3.3 + - 3.4 + - 3.5 + - 3.6 install: - - pip install tox codecov - -env: - - TOX_ENV=pypy - - TOX_ENV=py27 - - TOX_ENV=py33 - - TOX_ENV=py34 -# - TOX_ENV=py35 -# - TOX_ENV=flake8 + - pip install tox-travis codecov script: - - tox -e $TOX_ENV + - tox matrix: fast_finish: true include: - # https://github.com/travis-ci/travis-ci/issues/4794#issuecomment-143758799 - python: 3.5 env: - - TOX_ENV=py35 - - - python: 3.5 - env: - - TOX_ENV=flake8 + - TOXENV=flake8 after_success: - coverage combine && codecov \ No newline at end of file diff --git a/tox.ini b/tox.ini index 30dacf8..df04c05 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py33 py34 py35 + py36 flake8 From 0f6d9be4e241ea2e53ea3cc8576789ac6ab341a2 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 19 Feb 2017 18:06:57 +0100 Subject: [PATCH 149/270] Add `sudo: false` to .travis.yml for faster execution of jobs --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index a853a19..5200f17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ +sudo: false + language: python python: From f7d6406d387f6acb78ac53f75ec5fd391e6b32cd Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Wed, 22 Feb 2017 22:07:17 +0100 Subject: [PATCH 150/270] Work around https://github.com/travis-ci/travis-ci/issues/3080 as pypy throws if 'ping' can't be executed --- testscour.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testscour.py b/testscour.py index 6daec27..ace070e 100755 --- a/testscour.py +++ b/testscour.py @@ -2434,6 +2434,11 @@ class EmbedRasters(unittest.TestCase): import os import platform + # work around https://github.com/travis-ci/travis-ci/issues/3080 as pypy throws if 'ping' can't be executed + import distutils.spawn + if not distutils.spawn.find_executable('ping'): + return -1 + system = platform.system().lower() ping_count = '-n' if system == 'windows' else '-c' dev_null = 'NUL' if system == 'windows' else '/dev/null' From ffeb76c89464821cd350c1c992043def690a97a2 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Wed, 22 Feb 2017 22:13:04 +0100 Subject: [PATCH 151/270] Unittests: remove temporary file 'testscour_temp.svg' after running tests --- testscour.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testscour.py b/testscour.py index ace070e..29b70a9 100755 --- a/testscour.py +++ b/testscour.py @@ -95,6 +95,8 @@ class EmptyOptions(unittest.TestCase): fail = True sys.stdout = stdout_temp + os.remove('testscour_temp.svg') + self.assertEqual(fail, False, 'Exception when calling "start" with empty options object') From 7cb0d36d72e3fbef79e30fb744ea5f966051015c Mon Sep 17 00:00:00 2001 From: pborunda <pborunda@gmail.com> Date: Thu, 23 Feb 2017 14:00:32 -0700 Subject: [PATCH 152/270] Improve precision options for smaller output size (#131) Add a separate precision option for curve control points (--set-c-precision) This can considerably reduce file size with marginal effect on visual appearance. --- scour/scour.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index d3a3d71..467593b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2539,7 +2539,7 @@ def serializePath(pathObj, options): """ # 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]) + return ''.join([cmd + scourCoordinates(data, options, (cmd == 'a'), cmd) for cmd, data in pathObj]) def serializeTransform(transformObj): @@ -2554,7 +2554,7 @@ def serializeTransform(transformObj): ) -def scourCoordinates(data, options, forceCommaWsp=False): +def scourCoordinates(data, options, forceCommaWsp=False, cmd=''): """ Serializes coordinate data with some cleanups: - removes all trailing zeros after the decimal @@ -2567,7 +2567,10 @@ def scourCoordinates(data, options, forceCommaWsp=False): c = 0 previousCoord = '' for coord in data: - scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround) + cp = ((cmd == 'c' and (c % 6) < 4) or (cmd == 's' and (c % 4) < 2)) + scouredCoord = scourUnitlessLength(coord, + needsRendererWorkaround=options.renderer_workaround, + isControlPoint=cp) # 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 @@ -2613,7 +2616,7 @@ def scourLength(length): return scourUnitlessLength(length.value) + Unit.str(length.units) -def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a numeric type +def scourUnitlessLength(length, needsRendererWorkaround=False, isControlPoint=False): # length is of a numeric type """ Scours the numeric part of a length only. Does not accept units. @@ -2626,7 +2629,11 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of # reduce numeric precision # plus() corresponds to the unary prefix plus operator and applies context precision and rounding - length = scouringContext.plus(length) + sContext = scouringContext + if(isControlPoint): + sContext = scouringContextC + + length = sContext.plus(length) # remove trailing zeroes as we do not care for significance intLength = length.to_integral_value() @@ -3260,8 +3267,19 @@ def scourString(in_string, options=None): # calculations should be done in the default context (precision defaults to 28 significant digits) # to minimize errors global scouringContext + global scouringContextC + if(options.cdigits < 0): + # cdigits is negative value so use digits instead + options.cdigits = options.digits + scouringContext = Context(prec=options.digits) + # cdigits cannot have higher precision, limit to digits + if(options.cdigits < options.digits): + scouringContextC = Context(prec=options.cdigits) + else: + scouringContextC = scouringContext + # globals for tracking statistics # TODO: get rid of these globals... global _num_elements_removed @@ -3596,6 +3614,9 @@ _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)") +_option_group_optimization.add_option("-c", "--set-c-precision", + action="store", type=int, dest="cdigits", default=-1, metavar="NUM", + help="set no. of sig. digits (path [c/s] control points) (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") From a7e7b4c21d132e3c12f017baa02487b46930705b Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 23 Feb 2017 23:39:27 +0100 Subject: [PATCH 153/270] Cleanup options. Also omit short option strings of advanced options for now (if we offer them again in future, they should be chosen very carefully as should the options for which we offer them) --- scour/scour.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 467593b..b0e3391 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3597,12 +3597,16 @@ _options_parser = optparse.OptionParser( formatter=HeaderedFormatter(max_help_position=33), version=VER) +# legacy options (kept around for backwards compatibility, should not be used in new code) +_options_parser.add_option("-p", action="store", type=int, dest="digits", help=optparse.SUPPRESS_HELP) + +# general options _options_parser.add_option("-q", "--quiet", 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.)") + help="verbose output (statistics, etc.)") _options_parser.add_option("-i", action="store", dest="infilename", metavar="INPUT.SVG", help="alternative way to specify input filename") @@ -3611,15 +3615,16 @@ _options_parser.add_option("-o", help="alternative way to specify output filename") _option_group_optimization = optparse.OptionGroup(_options_parser, "Optimization") -_option_group_optimization.add_option("-p", "--set-precision", +_option_group_optimization.add_option("--set-precision", action="store", type=int, dest="digits", default=5, metavar="NUM", help="set number of significant digits (default: %default)") -_option_group_optimization.add_option("-c", "--set-c-precision", +_option_group_optimization.add_option("--set-c-precision", action="store", type=int, dest="cdigits", default=-1, metavar="NUM", - help="set no. of sig. digits (path [c/s] control points) (default: %default)") + help="set number of significant digits for control points " + "(default: same as '--set-precision')") _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") + help="won't convert 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") @@ -3712,8 +3717,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="If the input SVG uses non-standard flowing text exit with error. " - "Otherwise only warn.") + help="exit with error if the input SVG uses non-standard flowing text " + "(only warn by default)") _options_parser.add_option_group(_option_group_compatibility) From 2ebe9741b25bcd5b29f9032279b21001e0aae3bf Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Fri, 24 Feb 2017 02:55:44 +0100 Subject: [PATCH 154/270] Rename a variable plus some editing of comments --- scour/scour.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index b0e3391..7055db0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2554,13 +2554,13 @@ def serializeTransform(transformObj): ) -def scourCoordinates(data, options, forceCommaWsp=False, cmd=''): +def scourCoordinates(data, options, force_whitespace=False, cmd=''): """ 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) + - adds spaces between values in a subcommand if required (or if force_whitespace is True) """ if data is not None: newData = [] @@ -2571,13 +2571,12 @@ def scourCoordinates(data, options, forceCommaWsp=False, cmd=''): scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround, isControlPoint=cp) - # 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 + # don't output a space if this number starts with a dot (.) or minus sign (-); we only need a space if + # - this number starts with a digit + # - this number starts with a dot but the previous number had *no* dot or exponent + # i.e. '1.3 0.5' -> '1.3.5' or '1e3 0.5' -> '1e3.5' is fine but '123 0.5' -> '123.5' is obviously not + # - 'force_whitespace' is explicitly set to 'True' + if c > 0 and (force_whitespace or scouredCoord[0].isdigit() or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) ): @@ -2588,13 +2587,11 @@ def scourCoordinates(data, options, forceCommaWsp=False, cmd=''): 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. + # What we need to do to work around GNOME bugs 548494, 563933 and 620565, 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. + # TODO: Check whether this is still required (bugs all marked as fixed, might be time to phase it out) if options.renderer_workaround: if len(newData) > 0: for i in range(1, len(newData)): From 090884a70ff59b11fb6f561a5d81b8472a841eb6 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Fri, 24 Feb 2017 03:04:15 +0100 Subject: [PATCH 155/270] Don't force whitespace for elliptical paths (fixes #89) This was only required in an early draft of the SVG spec (an error that was corrected later, see [1,2]) [1] https://github.com/scour-project/scour/issues/89#issuecomment-244216600 [2] https://github.com/scour-project/scour/issues/89#issuecomment-244337118 --- scour/scour.py | 6 +++--- testscour.py | 9 --------- unittests/path-elliptical-arc-parsing.svg | 4 ---- 3 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 unittests/path-elliptical-arc-parsing.svg diff --git a/scour/scour.py b/scour/scour.py index 7055db0..ebc44c6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2539,7 +2539,7 @@ def serializePath(pathObj, options): """ # 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'), cmd) for cmd, data in pathObj]) + return ''.join([cmd + scourCoordinates(data, options, path_cmd=cmd) for cmd, data in pathObj]) def serializeTransform(transformObj): @@ -2554,7 +2554,7 @@ def serializeTransform(transformObj): ) -def scourCoordinates(data, options, force_whitespace=False, cmd=''): +def scourCoordinates(data, options, force_whitespace=False, path_cmd=''): """ Serializes coordinate data with some cleanups: - removes all trailing zeros after the decimal @@ -2567,7 +2567,7 @@ def scourCoordinates(data, options, force_whitespace=False, cmd=''): c = 0 previousCoord = '' for coord in data: - cp = ((cmd == 'c' and (c % 6) < 4) or (cmd == 's' and (c % 4) < 2)) + cp = ((path_cmd == 'c' and (c % 6) < 4) or (path_cmd == 's' and (c % 4) < 2)) scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround, isControlPoint=cp) diff --git a/testscour.py b/testscour.py index 29b70a9..d73090d 100755 --- a/testscour.py +++ b/testscour.py @@ -1640,15 +1640,6 @@ class PropagateCommonAttributesUp(unittest.TestCase): 'Did not move common fill attribute to grandparent') -class PathEllipticalArcParsingCommaWsp(unittest.TestCase): - - def runTest(self): - p = 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') - - class RemoveUnusedAttributesOnParent(unittest.TestCase): def runTest(self): diff --git a/unittests/path-elliptical-arc-parsing.svg b/unittests/path-elliptical-arc-parsing.svg deleted file mode 100644 index 77a8cbd..0000000 --- a/unittests/path-elliptical-arc-parsing.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" version="1.1"> - <path d="M100,100a100,100,0,1,1,-50,100z" fill="red" /> -</svg> From 12237e01c84f9156464489b85a065104d52ff04f Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 25 Feb 2017 17:59:44 +0100 Subject: [PATCH 156/270] =?UTF-8?q?Refactor=20logic=20to=20detect=20contro?= =?UTF-8?q?l=20points=20from=207cb0d36d72e3fbef79e30fb744ea5f966051015c=20?= =?UTF-8?q?and=20also=20include=20control=20points=20of=20quadratic=20B?= =?UTF-8?q?=C3=A9zier=20curve=20commands=20("q")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scour/scour.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index ebc44c6..b583f7b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2533,13 +2533,32 @@ def cleanPolyline(elem, options): elem.setAttribute('points', scourCoordinates(pts, options, True)) +def controlPoints(cmd, data): + """ + Checks if there are control points in the path + + Returns False if there aren't any + Returns a list of bools set to True for coordinates in the path data which are control points + """ + cmd = cmd.lower() + if cmd in ['c', 's', 'q']: + indices = range(0, len(data)) + if cmd == 'c': # c: (x1 y1 x2 y2 x y)+ + return [(index % 6) < 4 for index in indices] + elif cmd in ['s', 'q']: # s: (x2 y2 x y)+ q: (x1 y1 x y)+ + return [(index % 4) < 2 for index in indices] + + return False + + 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, path_cmd=cmd) for cmd, data in pathObj]) + return ''.join([cmd + scourCoordinates(data, options, reduce_precision=controlPoints(cmd, data)) + for cmd, data in pathObj]) def serializeTransform(transformObj): @@ -2554,7 +2573,7 @@ def serializeTransform(transformObj): ) -def scourCoordinates(data, options, force_whitespace=False, path_cmd=''): +def scourCoordinates(data, options, force_whitespace=False, reduce_precision=False): """ Serializes coordinate data with some cleanups: - removes all trailing zeros after the decimal @@ -2567,7 +2586,7 @@ def scourCoordinates(data, options, force_whitespace=False, path_cmd=''): c = 0 previousCoord = '' for coord in data: - cp = ((path_cmd == 'c' and (c % 6) < 4) or (path_cmd == 's' and (c % 4) < 2)) + cp = reduce_precision[c] if isinstance(reduce_precision, list) else reduce_precision scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround, isControlPoint=cp) From c2a65a772efd161e6e4bb7389e7be3f74458a49c Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 25 Feb 2017 18:07:03 +0100 Subject: [PATCH 157/270] Some code refactoring --- scour/scour.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index b583f7b..7ead404 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2565,12 +2565,8 @@ 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] - ) + return ' '.join([command + '(' + ' '.join([scourUnitlessLength(number) for number in numbers]) + ')' + for command, numbers in transformObj]) def scourCoordinates(data, options, force_whitespace=False, reduce_precision=False): @@ -2588,8 +2584,8 @@ def scourCoordinates(data, options, force_whitespace=False, reduce_precision=Fal for coord in data: cp = reduce_precision[c] if isinstance(reduce_precision, list) else reduce_precision scouredCoord = scourUnitlessLength(coord, - needsRendererWorkaround=options.renderer_workaround, - isControlPoint=cp) + renderer_workaround=options.renderer_workaround, + reduce_precision=cp) # don't output a space if this number starts with a dot (.) or minus sign (-); we only need a space if # - this number starts with a digit # - this number starts with a dot but the previous number had *no* dot or exponent @@ -2632,7 +2628,7 @@ def scourLength(length): return scourUnitlessLength(length.value) + Unit.str(length.units) -def scourUnitlessLength(length, needsRendererWorkaround=False, isControlPoint=False): # length is of a numeric type +def scourUnitlessLength(length, renderer_workaround=False, reduce_precision=False): # length is of a numeric type """ Scours the numeric part of a length only. Does not accept units. @@ -2645,11 +2641,10 @@ def scourUnitlessLength(length, needsRendererWorkaround=False, isControlPoint=Fa # reduce numeric precision # plus() corresponds to the unary prefix plus operator and applies context precision and rounding - sContext = scouringContext - if(isControlPoint): - sContext = scouringContextC - - length = sContext.plus(length) + if reduce_precision: + length = scouringContextC.plus(length) + else: + length = scouringContext.plus(length) # remove trailing zeroes as we do not care for significance intLength = length.to_integral_value() @@ -2663,7 +2658,7 @@ def scourUnitlessLength(length, needsRendererWorkaround=False, isControlPoint=Fa # (e.g. 123.4 should become 123, not 120 or even 100) nonsci = '{0:f}'.format(length) nonsci = '{0:f}'.format(initial_length.quantize(Decimal(nonsci))) - if not needsRendererWorkaround: + if not renderer_workaround: 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.': From 51c1e6af2306bca037aa452635f03256801d3494 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 25 Feb 2017 18:55:02 +0100 Subject: [PATCH 158/270] Improve options handling for precision options - prevent '--set-precision=0' by requiring >=1 - warn user if '--set-c-precision' > '--set-precision' instead of silently ignoring the value - some code cleanup --- scour/scour.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 7ead404..722b1bb 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3014,7 +3014,7 @@ def embedRasters(element, options): except Exception as e: print("WARNING: Could not open file '" + href + "' for embedding. " "The raster image will be kept as a reference but might be invalid. " - "(Exception details: " + str(e) + ")", file=sys.stderr) + "(Exception details: " + str(e) + ")", file=options.ensure_value("stdout", sys.stdout)) rasterdata = '' finally: # always restore initial working directory if we changed it above @@ -3274,22 +3274,17 @@ def scourString(in_string, options=None): # sanitize options (take missing attributes from defaults, discard unknown attributes) options = sanitizeOptions(options) - # create decimal context with reduced precision for scouring numbers + # default or invalid value + if(options.cdigits < 0): + options.cdigits = options.digits + + # create decimal contexts 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 - global scouringContextC - if(options.cdigits < 0): - # cdigits is negative value so use digits instead - options.cdigits = options.digits - + global scouringContextC # even more reduced precision for control points scouringContext = Context(prec=options.digits) - - # cdigits cannot have higher precision, limit to digits - if(options.cdigits < options.digits): - scouringContextC = Context(prec=options.cdigits) - else: - scouringContextC = scouringContext + scouringContextC = Context(prec=options.cdigits) # globals for tracking statistics # TODO: get rid of these globals... @@ -3743,8 +3738,12 @@ def parse_args(args=None, ignore_additional_args=False): 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 options.digits < 1: + _options_parser.error("Number of significant digits has to be larger than zero, see --help") + if options.cdigits > options.digits: + options.cdigits = -1 + print("WARNING: The value for '--set-c-precision' should be lower than the value for '--set-precision'. " + "Number of significant digits for control points reset to defsault value, see --help", file=sys.stderr) if options.indent_type not in ['tab', 'space', 'none']: _options_parser.error("Invalid value for --indent, see --help") if options.indent_depth < 0: From 98e3040645e3a3eed919f4f6f2ed66f0509f9601 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sat, 25 Feb 2017 19:33:03 +0100 Subject: [PATCH 159/270] Add unittest for `--set-c-precision` (7cb0d36d72e3fbef79e30fb744ea5f966051015c) --- testscour.py | 14 ++++++++++++++ unittests/path-precision-control-points.svg | 13 +++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 unittests/path-precision-control-points.svg diff --git a/testscour.py b/testscour.py index d73090d..894733b 100755 --- a/testscour.py +++ b/testscour.py @@ -1014,6 +1014,20 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): 'for path with ID ' + paths[5].getAttribute('id')) +class LimitPrecisionInControlPointPathData(unittest.TestCase): + + def runTest(self): + path_data = ("m1.1 2.2 3.3 4.4m-4.4-6.7" + "c1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7m-17-20" + "s1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4m-10-13" + "q1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4") + doc = scourXmlFile('unittests/path-precision-control-points.svg', + parse_args(['--set-precision=2', '--set-c-precision=1'])) + path_data2 = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path_data2, path_data, + 'Not correctly limiting precision on path data with --set-c-precision') + + class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): def runTest(self): diff --git a/unittests/path-precision-control-points.svg b/unittests/path-precision-control-points.svg new file mode 100644 index 0000000..add0f58 --- /dev/null +++ b/unittests/path-precision-control-points.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <path d="m 1.11 2.22 3.33 4.44 + M 0 0 + C 1.11 2.22 3.33 4.44 5.55 6.66 + c 1.11 2.22 3.33 4.44 5.55 6.66 1.11 2.22 3.33 4.44 5.55 6.66 + M 0 0 + S 1.11 2.22 3.33 4.44 + s 1.11 2.22 3.33 4.44 1.11 2.22 3.33 4.44 + M 0 0 + Q 1.11 2.22 3.33 4.44 + q 1.11 2.22 3.33 4.44 1.11 2.22 3.33 4.44" /> +</svg> From 5bfffc2ca825983c48ddc503b4b2209bb534cc60 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 30 Apr 2017 04:13:44 +0200 Subject: [PATCH 160/270] Hardcode printing of "flowtext" warning to stderr Third-party applications obviously can not handle additional output on stdout nor can they be expected to do any weird stdout/sterr redirection as we do via `options.stdout` We probably shouldn't print anything in `scourString()` to start with unless we offer an option to disable all non-SVG output for third-party libraries to use. --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 722b1bb..83591a6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3328,7 +3328,7 @@ def scourString(in_string, options=None): if options.error_on_flowtext: raise Exception(errmsg) else: - print("WARNING: {}".format(errmsg), file=options.ensure_value("stdout", sys.stdout)) + print("WARNING: {}".format(errmsg), file=sys.stderr) # remove descriptive elements removeDescriptiveElements(doc, options) From 62b16c11d85d7fa8155f8ed043317cdd9182b6d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi> Date: Tue, 9 May 2017 12:21:25 +0300 Subject: [PATCH 161/270] Spelling fixes --- scour/scour.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 83591a6..06f463e 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2742,7 +2742,7 @@ def optimizeAngle(angle): angle %= -360 else: angle %= 360 - # 720 degrees is unneccessary, as 360 covers all angles. + # 720 degrees is unnecessary, 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]). @@ -3498,7 +3498,7 @@ def scourString(in_string, options=None): viewBox = doc.documentElement.getAttribute('viewBox') if viewBox: lengths = re.split('[, ]+', viewBox) - lengths = [scourUnitlessLength(lenght) for lenght in lengths] + lengths = [scourUnitlessLength(length) for length in lengths] doc.documentElement.setAttribute('viewBox', ' '.join(lengths)) # more length scouring in this function @@ -3693,7 +3693,7 @@ _option_group_formatting.add_option("--nindent", _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\")") + "(also disables indentation; might be overridden 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") From 75bacbc8e6c7613344e2bdf52de09499b40b469f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi> Date: Tue, 9 May 2017 23:07:06 +0300 Subject: [PATCH 162/270] Python 3.6 invalid escape sequence deprecation fix (#144) (see https://docs.python.org/3/whatsnew/3.6.html#deprecated-python-behavior) --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 06f463e..6081a04 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1411,7 +1411,7 @@ def removeDuplicateGradients(doc): # 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") + 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']: From cc592c8e8a870aa7d76d54413c7bf3f69310898e Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 18 May 2017 00:53:25 +0200 Subject: [PATCH 163/270] Improve and fix behaviour when collapsing straight paths segments (#146) * Do not collapse straight path segments in paths that have intermediate markers (see #145). The intermediate nodes might be unnecessary for the shape of the path, but their markers would be lost. * Collapse subpaths of moveto `m` and lineto `l` commands if they have the same direction (before we only collapsed horizontal/vertical `h`/`v` lineto commands) * Attempt to collapse lineto `l` commands into a preceding moveto `m` command (these are then called "implicit lineto commands") * Preserve empty path segments if they have `stroke-linecap` set to `round` or `square`. They render no visible line but a tiny dot or square. --- scour/scour.py | 127 ++++++++++++------ testscour.py | 97 ++++++++----- unittests/collapse-straight-path-segments.svg | 33 +++++ unittests/consecutive-hlines.svg | 6 - unittests/path-precision.svg | 8 +- unittests/path-with-caps.svg | 8 +- 6 files changed, 194 insertions(+), 85 deletions(-) create mode 100644 unittests/collapse-straight-path-segments.svg delete mode 100644 unittests/consecutive-hlines.svg diff --git a/scour/scour.py b/scour/scour.py index 6081a04..11791c2 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -403,7 +403,16 @@ default_properties = { # excluded all properties with 'auto' as default } -def isSameSign(a, b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) +def is_same_sign(a, b): + return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) + + +def is_same_direction(x1, y1, x2, y2): + if is_same_sign(x1, x2) and is_same_sign(y1, y2): + diff = y1/x1 - y2/x2 + return scouringContext.plus(1 + diff) == 1 + else: + return False scinumber = re.compile(r"[-+]?(\d*\.?)?\d+[eE][-+]?\d+") @@ -2044,10 +2053,23 @@ def cleanPath(element, options): # this gets the parser object from svg_regex.py oldPathStr = element.getAttribute('d') path = svg_parser.parse(oldPathStr) + style = _getStyle(element) - # 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 or square linecaps. If it does, we do not want to collapse empty + # segments, as they are actually rendered (as circles or squares with diameter/dimension matching the path-width). + has_round_or_square_linecaps = ( + element.getAttribute('stroke-linecap') in ['round', 'square'] + or 'stroke-linecap' in style and style['stroke-linecap'] in ['round', 'square'] + ) + + # This determines whether the stroke has intermediate markers. If it does, we do not want to collapse + # straight segments running in the same direction, as markers are rendered on the intermediate nodes. + has_intermediate_markers = ( + element.hasAttribute('marker') + or element.hasAttribute('marker-mid') + or 'marker' in style + or 'marker-mid' in style + ) # The first command must be a moveto, and whether it's relative (m) # or absolute (M), the first set of coordinates *is* absolute. So @@ -2057,7 +2079,7 @@ def cleanPath(element, options): # 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)): + for pathIndex in range(len(path)): cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure i = 0 # adjust abs to rel @@ -2158,8 +2180,8 @@ def cleanPath(element, options): # 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)): + if not has_round_or_square_linecaps: + for pathIndex in range(len(path)): cmd, data = path[pathIndex] i = 0 if cmd in ['m', 'l', 't']: @@ -2253,26 +2275,25 @@ def cleanPath(element, options): 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: + if prevCmd == '': + # initialize with current path cmd and data prevCmd = cmd prevData = data + else: + # collapse if + # - cmd is not moveto (explicit moveto commands are not drawn) + # - the previous and current commands are the same type, + # - the previous command is moveto and the current is lineto + # (subsequent moveto pairs are treated as implicit lineto commands) + if cmd != 'm' and (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')): + prevData.extend(data) + # else flush the previous command if it is not the same type as the current command + else: + newPath.append((prevCmd, prevData)) + prevCmd = cmd + prevData = data # flush last command and data - if prevCmd != '': - newPath.append((prevCmd, prevData)) + newPath.append((prevCmd, prevData)) path = newPath # convert to shorthand path segments where possible @@ -2396,22 +2417,52 @@ def cleanPath(element, options): 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 + # For each m, l, 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. + # If the path has intermediate markers we have to preserve intermediate nodes, though. # 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] - _num_path_segments_removed += 1 - else: - coordIndex += 1 + if not has_intermediate_markers: + for pathIndex in range(len(path)): + cmd, data = path[pathIndex] + + # h / v expects only one parameter and we start drawing with the first (so we need at least 2) + if cmd in ['h', 'v'] and len(data) >= 2: + coordIndex = 0 + while coordIndex+1 < len(data): + if is_same_sign(data[coordIndex], data[coordIndex+1]): + data[coordIndex] += data[coordIndex+1] + del data[coordIndex+1] + _num_path_segments_removed += 1 + else: + coordIndex += 1 + + # l expects two parameters and we start drawing with the first (so we need at least 4) + elif cmd == 'l' and len(data) >= 4: + coordIndex = 0 + while coordIndex+2 < len(data): + if is_same_direction(*data[coordIndex:coordIndex+4]): + data[coordIndex] += data[coordIndex+2] + data[coordIndex+1] += data[coordIndex+3] + del data[coordIndex+2] # delete the next two elements + del data[coordIndex+2] + _num_path_segments_removed += 1 + else: + coordIndex += 2 + + # m expects two parameters but we have to skip the first pair as it's not drawn (so we need at least 6) + elif cmd == 'm' and len(data) >= 6: + coordIndex = 2 + while coordIndex+2 < len(data): + if is_same_direction(*data[coordIndex:coordIndex+4]): + data[coordIndex] += data[coordIndex+2] + data[coordIndex+1] += data[coordIndex+3] + del data[coordIndex+2] # delete the next two elements + del data[coordIndex+2] + _num_path_segments_removed += 1 + else: + coordIndex += 2 # 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 @@ -2542,7 +2593,7 @@ def controlPoints(cmd, data): """ cmd = cmd.lower() if cmd in ['c', 's', 'q']: - indices = range(0, len(data)) + indices = range(len(data)) if cmd == 'c': # c: (x1 y1 x2 y2 x y)+ return [(index % 6) < 4 for index in indices] elif cmd in ['s', 'q']: # s: (x2 y2 x y)+ q: (x1 y1 x y)+ diff --git a/testscour.py b/testscour.py index 894733b..8ebaf9d 100755 --- a/testscour.py +++ b/testscour.py @@ -964,10 +964,10 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=1'])) paths = doc.getElementsByTagNameNS(SVGNS, 'path') for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 12 123 1e3 1e4 1e5", + self.assertEqual(path.getAttribute('d'), "m1 21 321 4e3 5e4 7e5", 'Precision not correctly reduced with "--set-precision=1" ' 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1e3 -1e4 -1e5", + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4e3 -5e4 -7e5", 'Precision not correctly reduced with "--set-precision=1" ' 'for path with ID ' + paths[4].getAttribute('id')) self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", @@ -977,10 +977,10 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=2'])) paths = doc.getElementsByTagNameNS(SVGNS, 'path') for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 12 123 1234 12345 1.2e5", + self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 6.5e5", 'Precision not correctly reduced with "--set-precision=2" ' 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-1.2e5", + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-6.5e5", 'Precision not correctly reduced with "--set-precision=2" ' 'for path with ID ' + paths[4].getAttribute('id')) self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", @@ -990,10 +990,10 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=3'])) paths = doc.getElementsByTagNameNS(SVGNS, 'path') for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 12 123 1234 12345 123456", + self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", 'Precision not correctly reduced with "--set-precision=3" ' 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-123456", + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", 'Precision not correctly reduced with "--set-precision=3" ' 'for path with ID ' + paths[4].getAttribute('id')) self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", @@ -1003,10 +1003,10 @@ class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=4'])) paths = doc.getElementsByTagNameNS(SVGNS, 'path') for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 12 123 1234 12345 123456", + self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", 'Precision not correctly reduced with "--set-precision=4" ' 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-12-123-1234-12345-123456", + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", 'Precision not correctly reduced with "--set-precision=4" ' 'for path with ID ' + paths[4].getAttribute('id')) self.assertEqual(paths[5].getAttribute('d'), "m123.5 101-123.5-101", @@ -1036,16 +1036,25 @@ class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): 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): +class RemoveEmptySegmentsFromPathWithButtLineCaps(unittest.TestCase): def runTest(self): - 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') + doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) + for id in ['none', 'attr_butt', 'style_butt']: + path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) + self.assertEqual(len(path), 1, + 'Did not remove empty segments when path had butt linecaps') + + +class DoNotRemoveEmptySegmentsFromPathWithRoundSquareLineCaps(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) + for id in ['attr_round', 'attr_square', 'style_round', 'style_square']: + path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) + self.assertEqual(len(path), 2, + 'Did remove empty segments when path had round or square linecaps') class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase): @@ -1215,35 +1224,51 @@ class RemoveFontStylesFromNonTextShapes(unittest.TestCase): 'font-size not removed from rect') -class CollapseConsecutiveHLinesSegments(unittest.TestCase): +class CollapseStraightPathSegments(unittest.TestCase): def runTest(self): - p = scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), 'm100 100h200v100h-200z', - 'Did not collapse consecutive hlines segments') + doc = scourXmlFile('unittests/collapse-straight-path-segments.svg', parse_args(['--disable-style-to-xml'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + path_data = [path.getAttribute('d') for path in paths] + path_data_expected = ['m0 0h30', + 'm0 0v30', + 'm0 0h10.5v10.5', + 'm0 0h10-1v10-1', + 'm0 0h30', + 'm0 0h30', + 'm0 0h10 20', + 'm0 0h10 20', + 'm0 0h10 20', + 'm0 0h10 20', + 'm0 0 20 40v1l10 20', + 'm0 0 10 10-20-20 10 10-20-20', + 'm0 0 1 2m1 2 2 4m1 2 2 4', + 'm6.3228 7.1547 81.198 45.258'] + self.assertEqual(path_data[0:3], path_data_expected[0:3], + 'Did not collapse h/v commands into a single h/v commands') + self.assertEqual(path_data[3], path_data_expected[3], + 'Collapsed h/v commands with different direction') + self.assertEqual(path_data[4:6], path_data_expected[4:6], + 'Did not collapse h/v commands with only start/end markers present') + self.assertEqual(path_data[6:10], path_data_expected[6:10], + 'Did not preserve h/v commands with intermediate markers present') -class CollapseConsecutiveHLinesCoords(unittest.TestCase): - - def runTest(self): - p = 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 = 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') + self.assertEqual(path_data[10], path_data_expected[10], + 'Did not collapse lineto commands into a single (implicit) lineto command') + self.assertEqual(path_data[11], path_data_expected[11], + 'Collapsed lineto commands with different direction') + self.assertEqual(path_data[12], path_data_expected[12], + 'Collapsed first parameter pair of a moveto subpath') + self.assertEqual(path_data[13], path_data_expected[13], + 'Did not collapse the nodes of a straight real world path') class ConvertStraightCurvesToLines(unittest.TestCase): def runTest(self): p = scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), 'm10 10l40 40 40-40z', + self.assertEqual(p.getAttribute('d'), 'm10 10 40 40 40-40z', 'Did not convert straight curves into lines') @@ -1380,7 +1405,7 @@ class CollapseSamePathPoints(unittest.TestCase): def runTest(self): 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", + self.assertEqual(p.getAttribute('d'), "m100 100 100.12 100.12c14.877 4.8766-15.123-5.1234 0 0z", 'Did not collapse same path points') @@ -1986,7 +2011,7 @@ class PathEmptyMove(unittest.TestCase): def runTest(self): doc = scourXmlFile('unittests/path-empty-move.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100l200 100z') + self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100 200 100z') self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200l100 100z') diff --git a/unittests/collapse-straight-path-segments.svg b/unittests/collapse-straight-path-segments.svg new file mode 100644 index 0000000..fa8e030 --- /dev/null +++ b/unittests/collapse-straight-path-segments.svg @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"> + <defs> + <marker id="dot"> + <circle r="5px"/> + </marker> + </defs> + + <!-- h/v commands should be collapsed into a single h/v commands --> + <path d="m0 0h10 20"/> + <path d="m0 0v10 20"/> + <path d="m0 0h10 0.5v10 0.5"/> + <!-- h/v commands should not be collapsed if they have different direction --> + <path d="m0 0h10 -1v10 -1"/> + <!-- h/v commands should also be collapsed if only start/end markers are present --> + <path d="m0 0h10 20" marker-start="url(#dot)" marker-end="url(#dot)"/> + <path d="m0 0h10 20" style="marker-start:url(#dot);marker-end:url(#dot)"/> + <!-- h/v commands should be preserved if intermediate markers are present --> + <path d="m0 0h10 20" marker="url(#dot)"/> + <path d="m0 0h10 20" marker-mid="url(#dot)"/> + <path d="m0 0h10 20" style="marker:url(#dot)"/> + <path d="m0 0h10 20" style="marker-mid:url(#dot)"/> + + <!-- all consecutive lineto commands pointing into the sam direction + should be collapsed into a single (implicit if possible) lineto command --> + <path d="m 0 0 l 10 20 0.25 0.5 l 0.75 1.5 l 5 10 0.2 0.4 l 3 6 0.8 1.6 l 0 1 l 1 2 9 18"/> + <!-- must not be collapsed (same slope, but different direction) --> + <path d="m 0 0 10 10 -20 -20 l 10 10 -20 -20"/> + <!-- first parameter pair of a moveto subpath must not be collapsed as it's not drawn on canvas --> + <path d="m0 0 1 2 m 1 2 1 2l 1 2 m 1 2 1 2 1 2"/> + <!-- real world example of straight path with multiple nodes --> + <path d="m 6.3227953,7.1547422 10.6709787,5.9477588 9.20334,5.129731 22.977448,12.807101 30.447251,16.970601 7.898986,4.402712"/> +</svg> diff --git a/unittests/consecutive-hlines.svg b/unittests/consecutive-hlines.svg deleted file mode 100644 index caae623..0000000 --- a/unittests/consecutive-hlines.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg"> -<path fill="#F00" stroke="#0F0" d="M100,100h100h100v100h-200z"/> -<path fill="#F00" stroke="#0F0" d="M100,300h100,100v100h-200z"/> -<path fill="#F00" stroke="#0F0" d="M100,500h300h-100v100h-200z"/> -</svg> diff --git a/unittests/path-precision.svg b/unittests/path-precision.svg index 9f2bc38..9222ed3 100644 --- a/unittests/path-precision.svg +++ b/unittests/path-precision.svg @@ -2,10 +2,10 @@ <svg xmlns="http://www.w3.org/2000/svg"> <path id="p0" d="M 100.0000001 99.9999999 h100.01 v123456789.123456789 h-100 z" /> - <path id="p1" d="m 1 12 123 1234 12345 123456 " /> - <path id="p2" d="m 1.0 12.0 123.0 1234.0 12345.0 123456.0" /> - <path id="p3" d="m 01 012 0123 01234 012345 0123456 " /> - <path id="p4" d="m -1 -12 -123 -1234 -12345 -123456 " /> + <path id="p1" d="m 1 21 321 4321 54321 654321 " /> + <path id="p2" d="m 1.0 21.0 321.0 4321.0 54321.0 654321.0" /> + <path id="p3" d="m 01 021 0321 04321 054321 0654321 " /> + <path id="p4" d="m -1 -21 -321 -4321 -54321 -654321 " /> <path id="p6" d="m 123.456 101.001 -123.456 -101.001" /> </svg> diff --git a/unittests/path-with-caps.svg b/unittests/path-with-caps.svg index 0e7ab1a..3c24163 100644 --- a/unittests/path-with-caps.svg +++ b/unittests/path-with-caps.svg @@ -1,4 +1,10 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg"> - <path fill="none" stroke="#000" stroke-width="5" stroke-linecap="round" d="m 11,8 0,0" /> + <path id="none" d="m0 0 0 0"/> + <path id="attr_butt" d="m0 0 0 0" stroke-linecap="butt"/> + <path id="attr_round" d="m0 0 0 0" stroke-linecap="round"/> + <path id="attr_square" d="m0 0 0 0" stroke-linecap="square"/> + <path id="style_butt" d="m0 0 0 0" style="stroke-linecap:butt"/> + <path id="style_round" d="m0 0 0 0" style="stroke-linecap:round"/> + <path id="style_square" d="m0 0 0 0" style="stroke-linecap:square"/> </svg> From 992b6850c62c8c4c3cb2a94396cc4c0794f32c35 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 6 Aug 2017 03:52:20 +0200 Subject: [PATCH 164/270] Update HISTORY.md --- HISTORY.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 20a733d..c3015bd 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,7 +3,18 @@ ## Version 0.36 (not released yet) * Fix embedding of raster images which was broken in most cases and did not work at all in Python 3. ([#120](https://github.com/scour-project/scour/issues/62)) * Some minor fixes for statistics output. - +* Greatly improve the algorithm to reduce numeric precision. + * Precision was not properly reduced for some numbers. + * Only use reduced precision if it results in a shorter string representation, otherwise preserve full precision in output (e.g. use "123" instead of "1e2" when precision is set to 1). + * Reduce precision of lengths in `viewBox` ([#127](https://github.com/scour-project/scour/issues/127)) + * Add option `--set-c-precision` which allows to set a reduced numeric precision for control points.<br/>Control points determine how a path is bent in between two nodes and are less sensitive to a reduced precision than the position coordinates of the nodes themselves. This option can be used to save a few additional bytes without affecting visual appearance negatively. +* Fix: Unnecessary whitespace was not stripped from elliptical paths. ([#89](https://github.com/scour-project/scour/issues/89)) +* Improve and fix functionality to collapse straight paths segments. ([#146](https://github.com/scour-project/scour/issues/146)) + * Collapse subpaths of moveto `m` and lineto `l`commands if they have the same direction (before we only collapsed horizontal/vertical `h`/`v` lineto commands). + * Attempt to collapse lineto `l` commands into a preceding moveto `m` command (these are then called "implicit lineto commands") + * Do not collapse straight path segments in paths that have intermediate markers. ([#145](https://github.com/scour-project/scour/issues/145)) + * Preserve empty path segments if they have `stroke-linecap` set to `round` or `square`. They render no visible line but a tiny dot or square. + ## Version 0.35 (2016-09-14) From e0bfad272be153722af7d16cda425f0e7ff4c734 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 6 Aug 2017 04:13:23 +0200 Subject: [PATCH 165/270] Update README.md - the original page only has a link these days - project identity should be established by now --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f181151..d45b067 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Scour is a Python tool that takes an SVG file and produces a cleaner and more co Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). -Scour was originally developed by Jeff "codedread" Schiller and Louis Simard. Development is [now maintained](https://github.com/codedread/scour/issues/11) by Tobias "oberstet" Oberstein. - -This Github repository is the official one. The official website as well as older packages can be found at [www.codedread.com/scour](http://www.codedread.com/scour/). +Scour was originally developed by Jeff "codedread" Schiller and Louis Simard in in 2010. +The project moved to GitLab in 2013 an is now maintained by Tobias "oberstet" Oberstein and Eduard "Ede_123" Braun. ## Installation From c089448bb58cdc92e928a8ff961683c6ab35a4a8 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 6 Aug 2017 04:38:33 +0200 Subject: [PATCH 166/270] Update project description and use in both, README.md and setup.py --- README.md | 6 +++++- setup.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d45b067..fd8c324 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ --- -Scour is a Python tool that takes an SVG file and produces a cleaner and more concise file. It is intended to be used **after** exporting to SVG with a GUI editor, such as Inkscape or Adobe Illustrator. +Scour is an SVG optimizer/cleaner that reduces the size of scalable vector graphics by optimizing structure and removing unnecessary data written in Python. + +It can be used to create streamlined vector graphics suitable for web deployment, publishing/sharing or further processing. + +The goal of Scour is to output a file that renderes identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more agressive cleaning. Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). diff --git a/setup.py b/setup.py index 5149e88..0bcf8d1 100644 --- a/setup.py +++ b/setup.py @@ -22,15 +22,20 @@ import re from setuptools import find_packages, setup LONGDESC = """ -Scour is a SVG optimizer/sanitizer that can be used to produce SVGs for Web deployment. +Scour is an SVG optimizer/cleaner that reduces the size of scalable vector graphics by optimizing structure and removing unnecessary data. + +It can be used to create streamlined vector graphics suitable for web deployment, publishing/sharing or further processing. + +The goal of Scour is to output a file that renderes identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more agressive cleaning. Website - http://www.codedread.com/scour/ (original website) - - https://github.com/codedread/scour (today) + - https://github.com/scour-project/scour (today) Authors: - Jeff Schiller, Louis Simard (original authors) - Tobias Oberstein (maintainer) + - Eduard Braun (maintainer) """ VERSIONFILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "scour", "__init__.py") @@ -52,7 +57,7 @@ setup( license='Apache License 2.0', author='Jeff Schiller', author_email='codedread@gmail.com', - url='https://github.com/codedread/scour', + url='https://github.com/scour-project/scour', platforms=('Any'), install_requires=['six>=1.9.0'], packages=find_packages(), From 53d87ed35a72161d5e79be70942086ef31624e09 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 6 Aug 2017 04:47:33 +0200 Subject: [PATCH 167/270] make flake8 happy --- setup.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 0bcf8d1..b6faacb 100644 --- a/setup.py +++ b/setup.py @@ -22,11 +22,16 @@ import re from setuptools import find_packages, setup LONGDESC = """ -Scour is an SVG optimizer/cleaner that reduces the size of scalable vector graphics by optimizing structure and removing unnecessary data. +Scour is an SVG optimizer/cleaner that reduces the size of scalable +vector graphics by optimizing structure and removing unnecessary data. -It can be used to create streamlined vector graphics suitable for web deployment, publishing/sharing or further processing. +It can be used to create streamlined vector graphics suitable for web +deployment, publishing/sharing or further processing. -The goal of Scour is to output a file that renderes identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more agressive cleaning. +The goal of Scour is to output a file that renderes identically at a +fraction of the size by removing a lot of redundant information created +by most SVG editors. Optimization options are typically lossless but can +be tweaked for more agressive cleaning. Website - http://www.codedread.com/scour/ (original website) From 49f3664f8210fea6774a4a8fbe9bb5ed56e2d0f0 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 6 Aug 2017 04:55:43 +0200 Subject: [PATCH 168/270] make flake8 happier --- setup.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index b6faacb..01a7ae0 100644 --- a/setup.py +++ b/setup.py @@ -22,16 +22,16 @@ import re from setuptools import find_packages, setup LONGDESC = """ -Scour is an SVG optimizer/cleaner that reduces the size of scalable -vector graphics by optimizing structure and removing unnecessary data. +Scour is an SVG optimizer/cleaner that reduces the size of scalable +vector graphics by optimizing structure and removing unnecessary data. -It can be used to create streamlined vector graphics suitable for web -deployment, publishing/sharing or further processing. +It can be used to create streamlined vector graphics suitable for web +deployment, publishing/sharing or further processing. -The goal of Scour is to output a file that renderes identically at a -fraction of the size by removing a lot of redundant information created -by most SVG editors. Optimization options are typically lossless but can -be tweaked for more agressive cleaning. +The goal of Scour is to output a file that renderes identically at a +fraction of the size by removing a lot of redundant information created +by most SVG editors. Optimization options are typically lossless but can +be tweaked for more agressive cleaning. Website - http://www.codedread.com/scour/ (original website) From e36cd4832a4f8337bef6ca0125bc063da4de3f93 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 6 Aug 2017 04:55:59 +0200 Subject: [PATCH 169/270] Scour v0.36 --- HISTORY.md | 2 +- scour/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index c3015bd..3c7f37c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,6 @@ # Release Notes for Scour -## Version 0.36 (not released yet) +## Version 0.36 (2017-08-06) * Fix embedding of raster images which was broken in most cases and did not work at all in Python 3. ([#120](https://github.com/scour-project/scour/issues/62)) * Some minor fixes for statistics output. * Greatly improve the algorithm to reduce numeric precision. diff --git a/scour/__init__.py b/scour/__init__.py index aca9dcf..5831769 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ # ############################################################################### -__version__ = u'0.35' +__version__ = u'0.36' From 7ee5f9774d75997016d192b2d6f051e510cd5509 Mon Sep 17 00:00:00 2001 From: Michael Witten <mfwitten@gmail.com> Date: Thu, 24 Aug 2017 20:52:07 +0000 Subject: [PATCH 170/270] scour.py: Use named constants rather than literal integers for `nodeType' --- scour/scour.py | 67 +++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 11791c2..1dc0425 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -56,6 +56,7 @@ import re import sys import time import xml.dom.minidom +from xml.dom import Node from collections import namedtuple from decimal import Context, Decimal, InvalidOperation, getcontext @@ -540,7 +541,7 @@ def findElementsWithId(node, elems=None): 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: + if child.nodeType == Node.ELEMENT_NODE: findElementsWithId(child, elems) return elems @@ -604,7 +605,7 @@ def findReferencedElements(node, ids=None): if node.hasChildNodes(): for child in node.childNodes: - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: findReferencedElements(child, ids) return ids @@ -645,7 +646,7 @@ def removeUnusedDefs(doc, defElem, elemsToRemove=None): 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 + if elem.nodeType == Node.ELEMENT_NODE and (elem.getAttribute('id') == '' or 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 @@ -879,7 +880,7 @@ def removeUnreferencedIDs(referencedIDs, identifiedElements): def removeNamespacedAttributes(node, namespaces): global _num_attributes_removed num = 0 - if node.nodeType == 1: + if node.nodeType == Node.ELEMENT_NODE: # remove all namespace'd attributes from this element attrList = node.attributes attrsToRemove = [] @@ -901,7 +902,7 @@ def removeNamespacedAttributes(node, namespaces): def removeNamespacedElements(node, namespaces): global _num_elements_removed num = 0 - if node.nodeType == 1: + if node.nodeType == Node.ELEMENT_NODE: # remove all namespace'd child nodes from this element childList = node.childNodes childrenToRemove = [] @@ -959,12 +960,12 @@ def removeNestedGroups(node): groupsToRemove = [] # Only consider <g> elements for promotion if this element isn't a <switch>. # (partial fix for bug 594930, required by the SVG spec however) - if not (node.nodeType == 1 and node.nodeName == 'switch'): + if not (node.nodeType == Node.ELEMENT_NODE 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 \ + if grandchild.nodeType == Node.ELEMENT_NODE and grandchild.namespaceURI == NS['SVG'] and \ grandchild.nodeName in ['title', 'desc']: break else: @@ -979,7 +980,7 @@ def removeNestedGroups(node): # now recurse for children for child in node.childNodes: - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: num += removeNestedGroups(child) return num @@ -997,14 +998,14 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): childElements = [] # recurse first into the children (depth-first) for child in elem.childNodes: - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: # 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(): + elif child.nodeType == Node.TEXT_NODE and child.nodeValue.strip(): return num # only process the children if there are more than one element @@ -1102,7 +1103,7 @@ def createGroupsForCommonAttributes(elem): while curChild >= 0: childNode = elem.childNodes.item(curChild) - if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '' and childNode.nodeName in [ + if childNode.nodeType == Node.ELEMENT_NODE and childNode.getAttribute(curAttr) != '' and childNode.nodeName in [ # only attempt to group elements that the content model allows to be children of a <g> # SVG 1.1 (see https://www.w3.org/TR/SVG/struct.html#GElement) @@ -1130,7 +1131,7 @@ def createGroupsForCommonAttributes(elem): # attribute value, preserving any nodes in-between. while runStart > 0: nextNode = elem.childNodes.item(runStart - 1) - if nextNode.nodeType == 1: + if nextNode.nodeType == Node.ELEMENT_NODE: if nextNode.getAttribute(curAttr) != value: break else: @@ -1142,7 +1143,7 @@ def createGroupsForCommonAttributes(elem): 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: + if elem.childNodes.item(runEnd + 1).nodeType == Node.ELEMENT_NODE: break else: runEnd += 1 @@ -1186,7 +1187,7 @@ def createGroupsForCommonAttributes(elem): # each child gets the same treatment, recursively for childNode in elem.childNodes: - if childNode.nodeType == 1: + if childNode.nodeType == Node.ELEMENT_NODE: num += createGroupsForCommonAttributes(childNode) return num @@ -1202,7 +1203,7 @@ def removeUnusedAttributesOnParent(elem): childElements = [] # recurse first into the children (depth-first) for child in elem.childNodes: - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: childElements.append(child) num += removeUnusedAttributesOnParent(child) @@ -1302,11 +1303,11 @@ 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 is not None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ + if elem is not None and elem.nodeType == Node.ELEMENT_NODE 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'] \ + if refElem.nodeType == Node.ELEMENT_NODE and refElem.nodeName in ['linearGradient', 'radialGradient'] \ and refElem.namespaceURI == NS['SVG']: # elem is a gradient referenced by only one other gradient (refElem) @@ -1448,7 +1449,7 @@ def removeDuplicateGradients(doc): def _getStyle(node): u"""Returns the style attribute of a node as a dictionary.""" - if node.nodeType == 1 and len(node.getAttribute('style')) > 0: + if node.nodeType == Node.ELEMENT_NODE and len(node.getAttribute('style')) > 0: styleMap = {} rawStyles = node.getAttribute('style').split(';') for style in rawStyles: @@ -1614,7 +1615,7 @@ def styleInheritedFromParent(node, style): parentNode = node.parentNode # return None if we reached the Document element - if parentNode.nodeType == 9: + if parentNode.nodeType == Node.DOCUMENT_NODE: return None # check styles first (they take precedence over presentation attributes) @@ -1647,7 +1648,7 @@ def styleInheritedByChild(node, style, nodeIsChild=False): 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: + if node.nodeType != Node.ELEMENT_NODE: return False if nodeIsChild: @@ -1702,7 +1703,7 @@ def mayContainTextNodes(node): result = True # Default value # Comment, text and CDATA nodes don't have attributes and aren't containers - if node.nodeType != 1: + if node.nodeType != Node.ELEMENT_NODE: result = False # Non-SVG elements? Unknown elements! elif node.namespaceURI != NS['SVG']: @@ -1920,7 +1921,7 @@ def removeDefaultAttributeValues(node, options, tainted=set()): For such attributes, we don't delete attributes with default values.""" num = 0 - if node.nodeType != 1: + if node.nodeType != Node.ELEMENT_NODE: return 0 # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) @@ -1997,7 +1998,7 @@ def convertColors(element): """ numBytes = 0 - if element.nodeType != 1: + if element.nodeType != Node.ELEMENT_NODE: return 0 # set up list of color attributes for each element type @@ -2772,7 +2773,7 @@ def reducePrecision(element): _setStyle(element, styles) for child in element.childNodes: - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: num += reducePrecision(child) return num @@ -2989,7 +2990,7 @@ def optimizeTransforms(element, options): num += len(val) - len(newVal) for child in element.childNodes: - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: num += optimizeTransforms(child, options) return num @@ -3133,7 +3134,7 @@ def properlySizeDoc(docElement, options): def remapNamespacePrefix(node, oldprefix, newprefix): - if node is None or node.nodeType != 1: + if node is None or node.nodeType != Node.ELEMENT_NODE: return if node.prefix == oldprefix: @@ -3281,14 +3282,14 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): onNewLine = False for child in element.childNodes: # element node - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: 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: + elif child.nodeType == Node.TEXT_NODE: # trim it only in the case of not being a child of an element # where whitespace might be important if preserveWhitespace: @@ -3296,10 +3297,10 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): else: outParts.append(makeWellFormed(child.nodeValue.strip())) # CDATA node - elif child.nodeType == 4: + elif child.nodeType == Node.CDATA_SECTION_NODE: outParts.extend(['<![CDATA[', child.nodeValue, ']]>']) # Comment node - elif child.nodeType == 8: + elif child.nodeType == Node.COMMENT_NODE: outParts.extend(['<!--', child.nodeValue, '-->']) # TODO: entities, processing instructions, what else? else: # ignore the rest @@ -3468,9 +3469,9 @@ def scourString(in_string, options=None): removeElem = not elem.hasChildNodes() if removeElem is False: for child in elem.childNodes: - if child.nodeType in [1, 4, 8]: + if child.nodeType in [Node.ELEMENT_NODE, Node.CDATA_SECTION_NODE, Node.COMMENT_NODE]: break - elif child.nodeType == 3 and not child.nodeValue.isspace(): + elif child.nodeType == Node.TEXT_NODE and not child.nodeValue.isspace(): break else: removeElem = True @@ -3594,7 +3595,7 @@ def scourString(in_string, options=None): total_output = "" for child in doc.childNodes: - if child.nodeType == 1: + if child.nodeType == Node.ELEMENT_NODE: total_output += "".join(lines) else: # doctypes, entities, comments total_output += child.toxml() + '\n' From 695a91e447f708f1f1d179099096c4960effe377 Mon Sep 17 00:00:00 2001 From: Michael Witten <mfwitten@gmail.com> Date: Fri, 25 Aug 2017 04:56:47 +0000 Subject: [PATCH 171/270] scour.py: Satisfy the identing rules of PEP8 --- scour/scour.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1dc0425..fe468f0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -647,7 +647,7 @@ 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 == Node.ELEMENT_NODE and (elem.getAttribute('id') == '' or - elem.getAttribute('id') not 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']: @@ -1103,23 +1103,27 @@ def createGroupsForCommonAttributes(elem): while curChild >= 0: childNode = elem.childNodes.item(curChild) - if childNode.nodeType == Node.ELEMENT_NODE and childNode.getAttribute(curAttr) != '' and childNode.nodeName in [ - # only attempt to group elements that the content model allows to be children of a <g> + if ( + childNode.nodeType == Node.ELEMENT_NODE and + childNode.getAttribute(curAttr) != '' and + childNode.nodeName in [ + # only attempt to group elements that the content model allows to be children of a <g> - # 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.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' - ]: + # 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 @@ -1303,8 +1307,12 @@ 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 is not None and elem.nodeType == Node.ELEMENT_NODE and elem.nodeName in ['linearGradient', 'radialGradient'] \ - and elem.namespaceURI == NS['SVG']: + if ( + elem is not None and + elem.nodeType == Node.ELEMENT_NODE 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 == Node.ELEMENT_NODE and refElem.nodeName in ['linearGradient', 'radialGradient'] \ From fef2786c5ebe4b6d40980dbb66dd7142a75f2664 Mon Sep 17 00:00:00 2001 From: Michael Witten <mfwitten@gmail.com> Date: Thu, 24 Aug 2017 18:41:04 +0000 Subject: [PATCH 172/270] scour.py: minor rearrangement for the sake of clarity There has been a minor rearrangement of the code that handles the children of the element being serialized: The relevant `if' statement has had its condition effectively negated and thus has also had its consequent and alternative swapped; now, there is a very short consequent, followed by a very long alternative, rather than a very long consequent followed by a very short alternative. --- scour/scour.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index fe468f0..4a13125 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3282,9 +3282,12 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): elif attrValue == 'default': preserveWhitespace = False - # if no children, self-close children = element.childNodes - if children.length > 0: + if children.length == 0: + outParts.append('/>') + if indent > 0: + outParts.append(newline) + else: outParts.append('>') onNewLine = False @@ -3319,10 +3322,6 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): outParts.extend(['</', element.nodeName, '>']) if indent > 0: outParts.append(newline) - else: - outParts.append('/>') - if indent > 0: - outParts.append(newline) return "".join(outParts) From f14784b01f5ba80a026900f09a568bae78b4ff5d Mon Sep 17 00:00:00 2001 From: Michael Witten <mfwitten@gmail.com> Date: Thu, 24 Aug 2017 18:41:04 +0000 Subject: [PATCH 173/270] scour.py: handle `id' and `xml:id' in the same code that handles other attributes --- scour/scour.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 4a13125..b4a741c 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3215,25 +3215,11 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): 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]) - # 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', + 'id', 'xml:id', 'class', 'transform', 'x', 'y', 'z', 'width', 'height', 'x1', 'x2', 'y1', 'y2', 'dx', 'dy', 'rotate', 'startOffset', 'method', 'spacing', @@ -3253,8 +3239,6 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): 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: From 7e14cd352f361059f4c5df227ea4d751115a61b2 Mon Sep 17 00:00:00 2001 From: Michael Witten <mfwitten@gmail.com> Date: Thu, 24 Aug 2017 21:40:44 +0000 Subject: [PATCH 174/270] scour.py: Escape quote characters in attribute values, as necessary and minimally Either double quotes or single quotes are escaped; the choice is made so as to minimize the length of the escaped string. --- scour/scour.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index b4a741c..58ac7c4 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3178,23 +3178,25 @@ def remapNamespacePrefix(node, oldprefix, newprefix): 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 = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} +def makeWellFormed(str, quote=''): xml_ents = {'<': '<', '>': '>', '&': '&'} - -# starr = [] -# for c in str: -# 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: + if quote: + xml_ents[quote] = ''' if (quote == "'") else """ return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) +def chooseQuoteCharacter(str): + quotCount = str.count('"') + aposCount = str.count("'") + if quotCount > aposCount: + quote = "'" + hasEmbeddedQuote = aposCount + else: + quote = '"' + hasEmbeddedQuote = quotCount + return (quote, hasEmbeddedQuote) + + # hand-rolled serialization function that has the following benefits: # - pretty printing # - somewhat judicious use of whitespace @@ -3239,12 +3241,11 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): attrIndices += [attrName2Index[name] for name in sorted(attrName2Index.keys())] for index in attrIndices: attr = attrList.item(index) - # if the attribute value contains a double-quote, use single-quotes - quot = '"' - if attr.nodeValue.find('"') != -1: - quot = "'" - attrValue = makeWellFormed(attr.nodeValue) + attrValue = attr.nodeValue + (quote, hasEmbeddedQuote) = chooseQuoteCharacter(attrValue) + attrValue = makeWellFormed(attrValue, quote if hasEmbeddedQuote else '') + if attr.nodeName == 'style': # sort declarations attrValue = ';'.join([p for p in sorted(attrValue.split(';'))]) @@ -3258,7 +3259,7 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): outParts.append('xmlns:') elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': outParts.append('xlink:') - outParts.extend([attr.localName, '=', quot, attrValue, quot]) + outParts.extend([attr.localName, '=', quote, attrValue, quote]) if attr.nodeName == 'xml:space': if attrValue == 'preserve': From 0a146b7fef8b8122f49b07918d5828e0f6fa0e54 Mon Sep 17 00:00:00 2001 From: Michael Witten <mfwitten@gmail.com> Date: Thu, 24 Aug 2017 23:42:31 +0000 Subject: [PATCH 175/270] tests: Add unit tests for the escaping of quote characters in attribute values --- testscour.py | 38 +++++++++++++++++++++++++++++++++++++- unittests/entities.svg | 8 ++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 unittests/entities.svg diff --git a/testscour.py b/testscour.py index 8ebaf9d..8054371 100755 --- a/testscour.py +++ b/testscour.py @@ -1779,7 +1779,43 @@ class XmlEntities(unittest.TestCase): def runTest(self): self.assertEqual(makeWellFormed('<>&'), '<>&', - 'Incorrectly translated XML entities') + 'Incorrectly translated unquoted XML entities') + self.assertEqual(makeWellFormed('<>&', "'"), '<>&', + 'Incorrectly translated single-quoted XML entities') + self.assertEqual(makeWellFormed('<>&', '"'), '<>&', + 'Incorrectly translated double-quoted XML entities') + + self.assertEqual(makeWellFormed("'"), "'", + 'Incorrectly translated unquoted single quote') + self.assertEqual(makeWellFormed('"'), '"', + 'Incorrectly translated unquoted double quote') + + self.assertEqual(makeWellFormed("'", '"'), "'", + 'Incorrectly translated double-quoted single quote') + self.assertEqual(makeWellFormed('"', "'"), '"', + 'Incorrectly translated single-quoted double quote') + + self.assertEqual(makeWellFormed("'", "'"), ''', + 'Incorrectly translated single-quoted single quote') + self.assertEqual(makeWellFormed('"', '"'), '"', + 'Incorrectly translated double-quoted double quote') + + +class HandleQuotesInAttributes(unittest.TestCase): + + def runTest(self): + with open('unittests/entities.svg', "rb") as f: + output = scourString(f.read()) + self.assertTrue('a="\'"' in output, + 'Failed on attribute value with non-double quote') + self.assertTrue("b='\"'" in output, + 'Failed on attribute value with non-single quote') + self.assertTrue("c=\"''"\"" in output, + 'Failed on attribute value with more single quotes than double quotes') + self.assertTrue('d=\'""'\'' in output, + 'Failed on attribute value with more double quotes than single quotes') + self.assertTrue("e=\"''""\"" in output, + 'Failed on attribute value with the same number of double quotes as single quotes') class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): diff --git a/unittests/entities.svg b/unittests/entities.svg new file mode 100644 index 0000000..2308b46 --- /dev/null +++ b/unittests/entities.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" + a="'" + b='"' + c="''"" + d='""'' + e='''""' +/> From b20a0698ccf182b5fb248c173759f1fad8e4fac6 Mon Sep 17 00:00:00 2001 From: Michael Witten <mfwitten@gmail.com> Date: Sun, 27 Aug 2017 02:16:31 +0000 Subject: [PATCH 176/270] tests: Add unit tests for preservation of quotes in CSS styles These tests will ensure that issues #21 and #56 do not return. --- testscour.py | 11 +++++++++++ unittests/quotes-in-styles.svg | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 unittests/quotes-in-styles.svg diff --git a/testscour.py b/testscour.py index 8054371..50ad4e3 100755 --- a/testscour.py +++ b/testscour.py @@ -1818,6 +1818,17 @@ class HandleQuotesInAttributes(unittest.TestCase): 'Failed on attribute value with the same number of double quotes as single quotes') +class PreserveQuotesInStyles(unittest.TestCase): + + def runTest(self): + with open('unittests/quotes-in-styles.svg', "rb") as f: + output = scourString(f.read()) + self.assertTrue('use[id="t"]' in output, + 'Failed to preserve quote characters in a style element') + self.assertTrue("'Times New Roman'" in output, + 'Failed to preserve quote characters in a style attribute') + + class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): def runTest(self): diff --git a/unittests/quotes-in-styles.svg b/unittests/quotes-in-styles.svg new file mode 100644 index 0000000..38a30f2 --- /dev/null +++ b/unittests/quotes-in-styles.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <style>use[id="t"] {font-size: small}</style> + <text id="t" style="font-family:'Times New Roman'"/> +</svg> From f3d8936b5e84b75a9ea91d41439d563368178a33 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 17 Feb 2018 09:10:55 +0000 Subject: [PATCH 177/270] Rename "I" to "line_prefix" to avoid flake8 E741 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 58ac7c4..ef9c7be 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3205,17 +3205,17 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): outParts = [] indent = ind - I = '' + line_prefix = '' newline = '' if options.newlines: if options.indent_type == 'tab': - I = '\t' + line_prefix = '\t' elif options.indent_type == 'space': - I = ' ' - I *= options.indent_depth + line_prefix = ' ' + line_prefix *= options.indent_depth newline = '\n' - outParts.extend([(I * ind), '<', element.nodeName]) + outParts.extend([(line_prefix * ind), '<', element.nodeName]) # now serialize the other attributes known_attr = [ @@ -3303,7 +3303,7 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): pass if onNewLine: - outParts.append(I * ind) + outParts.append(line_prefix * ind) outParts.extend(['</', element.nodeName, '>']) if indent > 0: outParts.append(newline) From 843706be3963beca52788d2d259a0a83413e9f86 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 17 Feb 2018 09:15:45 +0000 Subject: [PATCH 178/270] Catch specific exception rather than anything The bare "except" also catches exceptions like "NameError" and "SystemExit", which we really should not catch. In scour.py, use the most specific exception (NotFoundErr) and in the tests just catch any "regular" exception. Reported by flake8. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 4 ++-- testscour.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index ef9c7be..b27cce5 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -56,7 +56,7 @@ import re import sys import time import xml.dom.minidom -from xml.dom import Node +from xml.dom import Node, NotFoundErr from collections import namedtuple from decimal import Context, Decimal, InvalidOperation, getcontext @@ -3619,7 +3619,7 @@ def scourXmlFile(filename, options=None): for node in all_nodes: try: node.setIdAttribute('id') - except: + except NotFoundErr: pass return doc diff --git a/testscour.py b/testscour.py index 50ad4e3..060b095 100755 --- a/testscour.py +++ b/testscour.py @@ -66,7 +66,7 @@ class EmptyOptions(unittest.TestCase): try: scourString(self.MINIMAL_SVG, options) fail = False - except: + except Exception: fail = True self.assertEqual(fail, False, 'Exception when calling "scourString" with empty options object') @@ -76,7 +76,7 @@ class EmptyOptions(unittest.TestCase): try: scourXmlFile('unittests/minimal.svg', options) fail = False - except: + except Exception: fail = True self.assertEqual(fail, False, 'Exception when calling "scourXmlFile" with empty options object') @@ -91,7 +91,7 @@ class EmptyOptions(unittest.TestCase): try: start(options, input, output) fail = False - except: + except Exception: fail = True sys.stdout = stdout_temp @@ -109,7 +109,7 @@ class InvalidOptions(unittest.TestCase): try: scourXmlFile('unittests/ids-to-strip.svg', options) fail = False - except: + except Exception: fail = True self.assertEqual(fail, False, 'Exception when calling Scour with invalid options') From 7249ae8b0a0bfb90a7781d30ff81c5335c31b070 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sat, 17 Feb 2018 11:59:35 +0100 Subject: [PATCH 179/270] user clearer variable names for indent type and indent depth --- scour/scour.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index b27cce5..742658c 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3201,21 +3201,20 @@ def chooseQuoteCharacter(str): # - pretty printing # - somewhat judicious use of whitespace # - ensure id attributes are first -def serializeXML(element, options, ind=0, preserveWhitespace=False): +def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): outParts = [] - indent = ind - line_prefix = '' + indent_type = '' newline = '' if options.newlines: if options.indent_type == 'tab': - line_prefix = '\t' + indent_type = '\t' elif options.indent_type == 'space': - line_prefix = ' ' - line_prefix *= options.indent_depth + indent_type = ' ' + indent_type *= options.indent_depth newline = '\n' - outParts.extend([(line_prefix * ind), '<', element.nodeName]) + outParts.extend([(indent_type * indent_depth), '<', element.nodeName]) # now serialize the other attributes known_attr = [ @@ -3270,7 +3269,7 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): children = element.childNodes if children.length == 0: outParts.append('/>') - if indent > 0: + if indent_depth > 0: outParts.append(newline) else: outParts.append('>') @@ -3282,7 +3281,7 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): if preserveWhitespace: outParts.append(serializeXML(child, options, 0, preserveWhitespace)) else: - outParts.extend([newline, serializeXML(child, options, indent + 1, preserveWhitespace)]) + outParts.extend([newline, serializeXML(child, options, indent_depth + 1, preserveWhitespace)]) onNewLine = True # text node elif child.nodeType == Node.TEXT_NODE: @@ -3303,9 +3302,9 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): pass if onNewLine: - outParts.append(line_prefix * ind) + outParts.append(indent_type * indent_depth) outParts.extend(['</', element.nodeName, '>']) - if indent > 0: + if indent_depth > 0: outParts.append(newline) return "".join(outParts) From 633b381d873fd3043570532f07850b2dff83defc Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 17 Feb 2018 10:59:13 +0000 Subject: [PATCH 180/270] findReferencedElements: Handle referencingProps separately Split the handling of referencingProps into a separate loop that calls findReferencingProperty directly. This saves a bunch of "make list, join list, append to another list and eventually split text into two elements" operations. This gives approximately 10% faster runtimes on 341 kB flamegraph generated by the "nytprof" Perl profiler. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 742658c..813204a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -593,8 +593,6 @@ def findReferencedElements(node, ids=None): # 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(':') @@ -603,6 +601,12 @@ def findReferencedElements(node, ids=None): val = propval[1].strip() findReferencingProperty(node, prop, val, ids) + for attr in referencingProps: + val = node.getAttribute(attr).strip() + if not val: + continue + findReferencingProperty(node, attr, val, ids) + if node.hasChildNodes(): for child in node.childNodes: if child.nodeType == Node.ELEMENT_NODE: From b916a189e9fda10cf007dd35799f0df44dfd5b31 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 17 Feb 2018 10:14:49 +0000 Subject: [PATCH 181/270] Avoid recomputing findReferencedElements in removeUnusedDefs The removeUnusedDefs function does not actually remove anything (that is left for its callers to do). This implies that findReferencedElements will return the same value before, during and after a call to removeUnusedDefs. Therefore, we can reuse the value from findReferencedElements when recursing into child nodes. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 813204a..4cb730f 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -641,11 +641,14 @@ def findReferencingProperty(node, prop, val, ids): ids[id] = [1, [node]] -def removeUnusedDefs(doc, defElem, elemsToRemove=None): +def removeUnusedDefs(doc, defElem, elemsToRemove=None, referencedIDs=None): if elemsToRemove is None: elemsToRemove = [] - referencedIDs = findReferencedElements(doc.documentElement) + # removeUnusedDefs do not change the XML itself; therefore there is no point in + # recomputing findReferencedElements when we recurse into child nodes. + if referencedIDs is None: + referencedIDs = findReferencedElements(doc.documentElement) keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] for elem in defElem.childNodes: @@ -655,7 +658,7 @@ def removeUnusedDefs(doc, defElem, elemsToRemove=None): # 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) + elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove, referencedIDs=referencedIDs) # we only remove if it is not one of our tags we always keep (see above) elif elem.nodeName not in keepTags: elemsToRemove.append(elem) From c54a7239e70bb1f0e40ed9ac191dbf96ebfb0dca Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 17 Feb 2018 10:02:16 +0000 Subject: [PATCH 182/270] Simplify the "ids" structure returned by findReferencedElements It was a dict with a two element list a la: { "id1": [len(nodeListX), nodeListX]], "id2": [len(nodeListY), nodeListY]], ... } This can trivially be simplified to: { "id1": nodeListX, "id2": nodeListY, ... } The two call-sites that actually needs the length (e.g. to sort by how often the id is used) can trivially compute that via a call to "len". All other call sites either just need to tell if an ID is used at all or work the nodes referencing the id (e.g. to remap the id). The former are unaffected by this change and the latter can now avoid a layer of indirection. This refactoring has negiable changes to the runtime and probably also to memory (not tested, but it is a minor constant improvement per referenced id). Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 4cb730f..6818c57 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -586,10 +586,9 @@ def findReferencedElements(node, ids=None): # 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) + ids[id].append(node) else: - ids[id] = [1, [node]] + ids[id] = [node] # now get all style properties and the fill, stroke, filter attributes styles = node.getAttribute('style').split(';') @@ -620,10 +619,9 @@ def findReferencingProperty(node, prop, val, ids): 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].append(node) else: - ids[id] = [1, [node]] + ids[id] = [node] # if the url has a quote in it, we need to compensate elif len(val) >= 8: id = None @@ -635,10 +633,9 @@ def findReferencingProperty(node, prop, val, ids): id = val[6:val.find("')")] if id is not None: if id in ids: - ids[id][0] += 1 - ids[id][1].append(node) + ids[id].append(node) else: - ids[id] = [1, [node]] + ids[id] = [node] def removeUnusedDefs(doc, defElem, elemsToRemove=None, referencedIDs=None): @@ -721,7 +718,7 @@ def shortenIDs(doc, prefix, unprotectedElements=None): # 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 + idList = [(len(referencedIDs[rid]), rid) for rid in referencedIDs if rid in unprotectedElements] idList.sort(reverse=True) idList = [rid for count, rid in idList] @@ -790,7 +787,7 @@ def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): # exactly like findReferencedElements would. # Cyn: Duplicated processing! - for node in referringNodes[1]: + for node in referringNodes: # 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 @@ -1307,12 +1304,10 @@ def collapseSinglyReferencedGradients(doc): 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] + for rid, nodes in six.iteritems(findReferencedElements(doc.documentElement)): # 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: + if len(nodes) == 1 and rid in identifiedElements: elem = identifiedElements[rid] if ( elem is not None and @@ -1437,7 +1432,7 @@ def removeDuplicateGradients(doc): # 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]: + for elem in referencedIDs[dup_id]: # find out which attribute referenced the duplicate gradient for attr in ['fill', 'stroke']: v = elem.getAttribute(attr) From cb093e9171bec11eeeff4b50eb0f575622721bf3 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sat, 17 Feb 2018 14:27:19 +0100 Subject: [PATCH 183/270] Update docstring of findReferencedElements (to match changes in c54a7239e70bb1f0e40ed9ac191dbf96ebfb0dca) --- scour/scour.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 6818c57..1be33c2 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -551,14 +551,12 @@ referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-st 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 IDs of all referenced elements + - node is the node at which to start the search. + - returns a map which has the id as key and + each value is is a list of nodes - Currently looks at fill, stroke, clip-path, mask, marker, and - xlink:href attributes. + Currently looks at 'xlink:href' and all attributes in 'referencingProps' """ global referencingProps if ids is None: From 2f0b3ea3620f14a4bffe5d4becb75ba051b20d15 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 17 Feb 2018 07:44:49 +0000 Subject: [PATCH 184/270] Rewrite redundant codepatterns introduced by py2 -> py3 conversion The automated python2 -> python3 converter creates some suboptimal code patterns in some cases, notably in its handling of dicts. This commit handles the following cases: * "if x in list(y.keys()):" => "if x in y:" The original code is neuters the O(1) lookup effeciency of a dict by turning it into a list. This occurs a O(n) in converting it to a list and then another O(n) for the lookup. When done in a loop, this becomes O(n * m) rather than the optimal O(m). * "for x in list(y.keys()):" => "for x in y:" OR "for x in list(y):" A dict (y in these cases) operates as an iterator over keys in the dict by default. This makes the entire "list(y.keys())" dance redundant _in most cases_. In a some cases, scour modifies the dict while iterating over it and in those cases, we need a "list(y)" (but not a "y.keys()"). The benefit of this differs between python2 and python3. In python3, we basically "only" avoid function call. In python2, y.keys() generates a list, so here we avoid generating a "throw-away list". The test suite succeed both with "python testscour.py" and "python3 testscour.py" (used 2.7.14+ and 3.6.4 from Debian testing). On a 341kB flame-graph generated by "nytprof" (a perl profiler), this commit changes the runtimes of scour from the range 3.39s - 3.45s to 3.27s - 3.35s making it roughly 3% faster in this case (YMMV, particularly with different input). The timings were recorded using the following command line: time PYTHONPATH=. python3 -m scour.scour --enable-id-stripping \ --shorten-ids --indent=none --enable-comment-stripping -i input.svg -o output.svg This was used 5 times with and 5 times without the patch picking the worst and best time to define the range. The runtime test was only preformed on python3. All changed lines where found with: grep -rE ' in list[(].*[.]keys[(][)][)]:' Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1be33c2..ebaed2e 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -846,7 +846,7 @@ def unprotected_ids(doc, options): 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()): + for id in list(identifiedElements): protected = False if options.protect_ids_noninkscape and not id[-1].isdigit(): protected = True @@ -870,7 +870,7 @@ def removeUnreferencedIDs(referencedIDs, identifiedElements): global _num_ids_removed keepTags = ['font'] num = 0 - for id in list(identifiedElements.keys()): + for id in identifiedElements: node = identifiedElements[id] if id not in referencedIDs and node.nodeName not in keepTags: node.removeAttribute('id') @@ -1051,7 +1051,7 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): distinctAttrs = [] # loop through all current 'common' attributes - for name in list(commonAttrs.keys()): + for name in commonAttrs: # if this child doesn't match that attribute, schedule it for removal if child.getAttribute(name) != commonAttrs[name]: distinctAttrs.append(name) @@ -1060,7 +1060,7 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): del commonAttrs[name] # commonAttrs now has all the inheritable attributes which are common among all child elements - for name in list(commonAttrs.keys()): + for name in commonAttrs: for child in childElements: child.removeAttribute(name) elem.setAttribute(name, commonAttrs[name]) @@ -1239,7 +1239,7 @@ def removeUnusedAttributesOnParent(elem): for childNum in range(len(childElements)): child = childElements[childNum] inheritedAttrs = [] - for name in list(unusedAttrs.keys()): + for name in unusedAttrs: val = child.getAttribute(name) if val == '' or val is None or val == 'inherit': inheritedAttrs.append(name) @@ -1247,7 +1247,7 @@ def removeUnusedAttributesOnParent(elem): del unusedAttrs[a] # unusedAttrs now has all the parent attributes that are unused - for name in list(unusedAttrs.keys()): + for name in unusedAttrs: elem.removeAttribute(name) num += 1 @@ -1419,7 +1419,7 @@ def removeDuplicateGradients(doc): # get a collection of all elements that are referenced and their referencing elements referencedIDs = findReferencedElements(doc.documentElement) - for masterGrad in list(gradientsToRemove.keys()): + for masterGrad in gradientsToRemove: master_id = masterGrad.getAttribute('id') for dupGrad in gradientsToRemove[masterGrad]: # if the duplicate gradient no longer has a parent that means it was @@ -1599,7 +1599,7 @@ def repairStyle(node, options): # 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()): + for propName in list(styleMap): if propName in svgAttributes: node.setAttribute(propName, styleMap[propName]) del styleMap[propName] @@ -1940,7 +1940,7 @@ def removeDefaultAttributeValues(node, options, tainted=set()): 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 attribute in default_properties: if node.getAttribute(attribute) == default_properties[attribute]: node.removeAttribute(attribute) num += 1 @@ -1948,9 +1948,9 @@ def removeDefaultAttributeValues(node, options, tainted=set()): tainted = taint(tainted, attribute) # Properties might also occur as styles, remove them too styles = _getStyle(node) - for attribute in list(styles.keys()): + for attribute in list(styles): if attribute not in tainted: - if attribute in list(default_properties.keys()): + if attribute in default_properties: if styles[attribute] == default_properties[attribute]: del styles[attribute] num += 1 @@ -1975,7 +1975,7 @@ def convertColor(value): """ s = value - if s in list(colors.keys()): + if s in colors: s = colors[s] rgbpMatch = rgbp.match(s) @@ -2031,7 +2031,7 @@ def convertColors(element): element.setAttribute(attr, newColorValue) numBytes += (oldBytes - len(element.getAttribute(attr))) # colors might also hide in styles - if attr in list(styles.keys()): + if attr in styles: oldColorValue = styles[attr] newColorValue = convertColor(oldColorValue) oldBytes = len(oldColorValue) @@ -2770,7 +2770,7 @@ def reducePrecision(element): num += len(val) - len(newVal) element.setAttribute(lengthAttr, newVal) # repeat for attributes hidden in styles - if lengthAttr in list(styles.keys()): + if lengthAttr in styles: val = styles[lengthAttr] valLen = SVGLength(val) if valLen.units != Unit.INVALID: From 5360db86d94c6303362925627288fa1ad135c86f Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sat, 17 Feb 2018 15:59:18 +0100 Subject: [PATCH 185/270] Fix the last instance of "list(dict.keys())" --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index ebaed2e..1998482 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1471,7 +1471,7 @@ def _getStyle(node): 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())]) + fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap]) if fixedStyle != '': node.setAttribute('style', fixedStyle) elif node.getAttribute('style'): From 0776d32179e2576b40cb2c9c9cdd91cae3a2e197 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 17 Feb 2018 17:21:25 +0000 Subject: [PATCH 186/270] Remove an unnecessary loop The unprotected_ids function returns all unprotected ids and removeUnreferencedIDs removes all of them that does not appear in the return value of findReferencedElements. On closer observation it turns out that removeUnreferencedIDs cannot cause nodes/IDs to become unprotected nor unreferenced (as it only remove the "id" attribute, not the node). With this in mind, we can just remove the loop and save a call to all of these functions. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1998482..1c65ccd 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3471,11 +3471,9 @@ def scourString(in_string, options=None): _num_elements_removed += 1 if options.strip_ids: - bContinueLooping = True - while bContinueLooping: - identifiedElements = unprotected_ids(doc, options) - referencedIDs = findReferencedElements(doc.documentElement) - bContinueLooping = (removeUnreferencedIDs(referencedIDs, identifiedElements) > 0) + referencedIDs = findReferencedElements(doc.documentElement) + identifiedElements = unprotected_ids(doc, options) + removeUnreferencedIDs(referencedIDs, identifiedElements) while removeDuplicateGradientStops(doc) > 0: pass From 7115e82cb86ab05ba8103118cb157757d0211fa0 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 10 Mar 2018 13:35:37 +0100 Subject: [PATCH 187/270] Fix typo in HISTORY.md --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 3c7f37c..ed88f93 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -82,7 +82,7 @@ ## Version 0.29 (2014-07-26) * Add option `--keep-unreferenced-defs` to preserve elements in `<defs>` that are not referenced and would be removed otherwise. ([#2](https://github.com/scour-project/scour/issues/2)) -* Add option to ingore unknown cmd line opts. +* Add option to ignore unknown cmd line opts. ## Version 0.28 (2014-01-12) From cae0faefa0f14c6d21d995b3a12ae6d0bd4c76e0 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 10 Mar 2018 15:47:30 +0100 Subject: [PATCH 188/270] =?UTF-8?q?Avoid=20O(n=C2=B2)=20in=20removeDuplica?= =?UTF-8?q?teGradient=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original implementation of removeDuplicateGradient does O(n²) search over all gradients to remove duplicates. In images with many gradients (such as [MediaWiki_logo_1.svg]), this becomes a significant overhead as that logo has over 900 duplicated gradients. We solve this by creating a key for each gradient based on the attributes we use for duplication detection. This key is generated such that if two gradients have the same key, they are duplicates (for our purpose) and the keys are different then the gradients are guaranteed to be different as well. With such a key, we can rely on a dict to handle the duplication detection (which it does very well). This change improves the runtime performance on [MediaWiki_logo_1.svg] by about 25% (8m51s -> 1m56s on 5 runs). Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 91 +++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1c65ccd..60cdff0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -57,7 +57,7 @@ import sys import time import xml.dom.minidom from xml.dom import Node, NotFoundErr -from collections import namedtuple +from collections import namedtuple, defaultdict from decimal import Context, Decimal, InvalidOperation, getcontext import six @@ -1355,6 +1355,32 @@ def collapseSinglyReferencedGradients(doc): return num +def computeGradientBucketKey(grad): + # Compute a key (hashable opaque value; here a string) from each + # gradient such that "key(grad1) == key(grad2)" is the same as + # saying that grad1 is a duplicate of grad2. + gradBucketAttr = ['gradientUnits', 'spreadMethod', 'gradientTransform', + 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy', 'r'] + gradStopBucketsAttr = ['offset', 'stop-color', 'stop-opacity', 'style'] + + # A linearGradient can never be a duplicate of a + # radialGradient (and vice versa) + subKeys = [grad.getAttribute(a) for a in gradBucketAttr] + subKeys.append(grad.getAttributeNS(NS['XLINK'], 'href')) + stops = grad.getElementsByTagName('stop') + if stops.length: + for i in range(stops.length): + stop = stops.item(i) + for attr in gradStopBucketsAttr: + stopKey = stop.getAttribute(attr) + subKeys.append(stopKey) + + # Use a raw ASCII "record separator" control character as it is + # not likely to be used in any of these values (without having to + # be escaped). + return "\x1e".join(subKeys) + + def removeDuplicateGradients(doc): global _num_elements_removed num = 0 @@ -1364,58 +1390,23 @@ def removeDuplicateGradients(doc): for gradType in ['linearGradient', 'radialGradient']: grads = doc.getElementsByTagName(gradType) + gradBuckets = defaultdict(list) + 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 + key = computeGradientBucketKey(grad) + gradBuckets[key].append(grad) - # 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 + for bucket in six.itervalues(gradBuckets): + if len(bucket) < 2: + # The gradient must be unique if it is the only one in + # this bucket. + continue + master = bucket[0] + duplicates = bucket[1:] - 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 + gradientsToRemove[master] = duplicates + for ograd in duplicates: + duplicateToMaster[ograd] = master # get a collection of all elements that are referenced and their referencing elements referencedIDs = findReferencedElements(doc.documentElement) From 6ea126d290e29c990244d9df17f00f84b7c07108 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 10 Mar 2018 16:06:50 +0100 Subject: [PATCH 189/270] Gracefully handle unreferenced gradients with --keep-unreferenced-defs (#173) Closes: #156 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 60cdff0..1e990c4 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1421,22 +1421,26 @@ def removeDuplicateGradients(doc): # 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]: - # 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) + + # With --keep-unreferenced-defs, we can end up with + # unreferenced gradients. See GH#156. + if dup_id in referencedIDs: + for elem in referencedIDs[dup_id]: + # 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 From a2c94c96fb75a8b62b99b2af81d8f5d291fd2bb3 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 11 Mar 2018 07:18:27 +0000 Subject: [PATCH 190/270] Disable the "m0 0"-optimization as it is wrong in some cases The "m0 0" rewrite gets some cases wrong, like: m150 240h200m0 0 150 150v-300z Scour rewrote that into the following m150 240h200l150 150v-300z However, these two paths do not produce an identical figure at all. The first is a line followed by a triangle while the second is a quadrilateral. While there are some instances we can rewrite (that scour will no longer rewrite), these will require an analysis over multiple commands to determine whether the rewrite is safe. This will reappear in the next commit. Closes: #163 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 17 +++++++++-------- testscour.py | 5 +++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1e990c4..806747e 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2190,14 +2190,15 @@ def cleanPath(element, options): 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:]) - _num_path_segments_removed += 1 - else: # else skip move coordinate - i = 2 + # It might be tempting to rewrite "m0 0 ..." into + # "l..." here. However, this is an unsound + # optimization in general as "m0 0 ... z" is + # different from "l...z". + # + # To do such a rewrite, we need to understand the + # full subpath, so for now just leave the first + # two coordinates of "m" alone. + i = 2 while i < len(data): if data[i] == data[i + 1] == 0: del data[i:i + 2] diff --git a/testscour.py b/testscour.py index 060b095..b6abfd5 100755 --- a/testscour.py +++ b/testscour.py @@ -2058,8 +2058,9 @@ class PathEmptyMove(unittest.TestCase): def runTest(self): doc = scourXmlFile('unittests/path-empty-move.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100 200 100z') - self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200l100 100z') + # This path can actually be optimized to avoid the "m0 0z". + self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100 200 100m0 0z') + self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200m0 0 100 100z') class DefaultsRemovalToplevel(unittest.TestCase): From 38274f75bc90f998b4a0448481d2dd8800c21d7a Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 11 Mar 2018 08:22:27 +0000 Subject: [PATCH 191/270] Implement a basic rewrite of redundant commands This basic implementation can drop and rewrite some cases of "m0 0" and "z" without triggering the issues experienced in #163. It works by analysing the path backwards and tracking "z" and "m" commands. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 46 +++++++++++++++++++++++++++-- testscour.py | 22 ++++++++++---- unittests/path-command-rewrites.svg | 8 +++++ unittests/path-empty-move.svg | 5 ---- 4 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 unittests/path-command-rewrites.svg delete mode 100644 unittests/path-empty-move.svg diff --git a/scour/scour.py b/scour/scour.py index 806747e..dcef9ce 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2181,10 +2181,11 @@ def cleanPath(element, options): x, y = startx, starty path[pathIndex] = ('z', data) - # remove empty segments + # remove empty segments and redundant commands # Reuse the data structure 'path' and the coordinate lists, even if we're # deleting items, because these deletions are relatively cheap. if not has_round_or_square_linecaps: + # remove empty path segments for pathIndex in range(len(path)): cmd, data = path[pathIndex] i = 0 @@ -2196,8 +2197,8 @@ def cleanPath(element, options): # different from "l...z". # # To do such a rewrite, we need to understand the - # full subpath, so for now just leave the first - # two coordinates of "m" alone. + # full subpath. This logic happens after this + # loop. i = 2 while i < len(data): if data[i] == data[i + 1] == 0: @@ -2231,6 +2232,45 @@ def cleanPath(element, options): path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) _num_path_segments_removed += len(path[pathIndex][1]) - oldLen + # remove no-op commands + pathIndex = len(path) + subpath_needs_anchor = False + # NB: We can never rewrite the first m/M command (expect if it + # is the only command) + while pathIndex > 1: + pathIndex -= 1 + cmd, data = path[pathIndex] + if cmd == 'z': + next_cmd, next_data = path[pathIndex - 1] + if next_cmd == 'm' and len(next_data) == 2: + # mX Yz -> mX Y + + # note the len check on next_data as it is not + # safe to rewrite "m0 0 1 1z" in general (it is a + # question of where the "pen" ends - you can + # continue a draw on the same subpath after a + # "z"). + del path[pathIndex] + _num_path_segments_removed += 1 + else: + # it is not safe to rewrite "m0 0 ..." to "l..." + # because of this "z" command. + subpath_needs_anchor = True + elif cmd == 'm': + if len(path) - 1 == pathIndex and len(data) == 2: + # Ends with an empty move (but no line/draw + # following it) + del path[pathIndex] + _num_path_segments_removed += 1 + continue + if subpath_needs_anchor: + subpath_needs_anchor = False + elif data[0] == data[1] == 0: + # unanchored, i.e. we can replace "m0 0 ..." with + # "l..." as there is no "z" after it. + path[pathIndex] = ('l', data[2:]) + _num_path_segments_removed += 1 + # fixup: Delete subcommands having no coordinates. path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] diff --git a/testscour.py b/testscour.py index b6abfd5..1e0caa7 100755 --- a/testscour.py +++ b/testscour.py @@ -2054,13 +2054,25 @@ class StyleToAttr(unittest.TestCase): self.assertEqual(line.getAttribute('marker-end'), 'url(#m)') -class PathEmptyMove(unittest.TestCase): +class PathCommandRewrites(unittest.TestCase): def runTest(self): - doc = scourXmlFile('unittests/path-empty-move.svg') - # This path can actually be optimized to avoid the "m0 0z". - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100 200 100m0 0z') - self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200m0 0 100 100z') + doc = scourXmlFile('unittests/path-command-rewrites.svg') + paths = doc.getElementsByTagName('path') + expected_paths = [ + ('m100 100 200 100', "Trailing m0 0z not removed"), + ('m100 100v200m0 0 100 100z', "Mangled m0 0 100 100"), + ("m100 100v200m0 0 2-1-2 1z", "Should have removed empty m0 0"), + ("m100 100v200l3-5-5 3m0 0 2-1-2 1z", "Rewrite m0 0 3-5-5 3 ... -> l3-5-5 3 ..."), + ("m100 100v200m0 0 3-5-5 3zm0 0 2-1-2 1z", "No rewrite of m0 0 3-5-5 3z"), + ] + self.assertEqual(len(paths), len(expected_paths), "len(actual_paths) != len(expected_paths)") + for i in range(len(paths)): + actual_path = paths[i].getAttribute('d') + expected_path, message = expected_paths[i] + self.assertEqual(actual_path, + expected_path, + '%s: "%s" != "%s"' % (message, actual_path, expected_path)) class DefaultsRemovalToplevel(unittest.TestCase): diff --git a/unittests/path-command-rewrites.svg b/unittests/path-command-rewrites.svg new file mode 100644 index 0000000..47ddc61 --- /dev/null +++ b/unittests/path-command-rewrites.svg @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <path d="m100 100 l200 100 m0 0z" /> + <path d="m100 100 v200 m0 0 100 100z" /> + <path d="m100 100 v200 m0 0m0 0 2-1-2 1z" /> + <path d="m100 100 v200 m0 0 3-5-5 3m0 0 2-1-2 1z" /> + <path d="m100 100 v200 m0 0 3-5-5 3zm0 0 2-1-2 1z" /> +</svg> diff --git a/unittests/path-empty-move.svg b/unittests/path-empty-move.svg deleted file mode 100644 index d3b63d7..0000000 --- a/unittests/path-empty-move.svg +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0"?> -<svg xmlns="http://www.w3.org/2000/svg"> - <path d="m100 100 l200 100 m0 0z" /> - <path d="m100 100 v200 m0 0 100 100z" /> -</svg> From b622642aa1c661703198ef05efffba1ca4e9a8d7 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Mon, 26 Mar 2018 22:30:25 +0200 Subject: [PATCH 192/270] Simplify timer selection to always use time.time() (#175) In python2.7 and python3.3, time.time() is sufficient accurate for our purpose and avoids going through hoops to select the best available function. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1e990c4..7f136a5 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -74,11 +74,10 @@ 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 +# the walltime measurement function, we will use for reporting +# reporting how long it took to process a given SVG file. For our +# purposes, the time.time() function has sufficent accuracy. +walltime = time.time NS = {'SVG': 'http://www.w3.org/2000/svg', From d508f59aa699eb6ba4d6ba5f3f9737d811eee39b Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Mon, 26 Mar 2018 22:34:11 +0200 Subject: [PATCH 193/270] Completely remove "walltime" variable and use time.time() directly --- scour/scour.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 7f136a5..afadb64 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -74,12 +74,6 @@ VER = __version__ COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' -# the walltime measurement function, we will use for reporting -# reporting how long it took to process a given SVG file. For our -# purposes, the time.time() function has sufficent accuracy. -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', @@ -3865,7 +3859,7 @@ def start(options, input, output): # sanitize options (take missing attributes from defaults, discard unknown attributes) options = sanitizeOptions(options) - start = walltime() + start = time.time() # do the work in_string = input.read() @@ -3878,7 +3872,7 @@ def start(options, input, output): if not ((output is sys.stdout) or (hasattr(sys.stdout, 'buffer') and output is sys.stdout.buffer)): output.close() - end = walltime() + end = time.time() # run-time in ms duration = int(round((end - start) * 1000.)) From f8d5af0e56e959dcdd5dd85f508472d06843a47a Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 11 Mar 2018 22:01:35 +0000 Subject: [PATCH 194/270] Remove now unused variable Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index afadb64..2fa3235 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1379,7 +1379,6 @@ def removeDuplicateGradients(doc): num = 0 gradientsToRemove = {} - duplicateToMaster = {} for gradType in ['linearGradient', 'radialGradient']: grads = doc.getElementsByTagName(gradType) @@ -1398,8 +1397,6 @@ def removeDuplicateGradients(doc): duplicates = bucket[1:] gradientsToRemove[master] = duplicates - for ograd in duplicates: - duplicateToMaster[ograd] = master # get a collection of all elements that are referenced and their referencing elements referencedIDs = findReferencedElements(doc.documentElement) From ba7f4b5f181e90625c43da8b4caaed1874a0dd51 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 11 Mar 2018 22:37:00 +0000 Subject: [PATCH 195/270] Remove more redundant uses of .keys() Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 2fa3235..a1c11a0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1613,7 +1613,7 @@ def styleInheritedFromParent(node, style): # check styles first (they take precedence over presentation attributes) styles = _getStyle(parentNode) - if style in styles.keys(): + if style in styles: value = styles[style] if not value == 'inherit': return value @@ -1653,7 +1653,7 @@ def styleInheritedByChild(node, style, nodeIsChild=False): return False # check styles styles = _getStyle(node) - if (style in styles.keys()) and not (styles[style] == 'inherit'): + if (style in styles) and not (styles[style] == 'inherit'): return False else: # if the passed-in node does not have any children 'style' can obviously not be inherited @@ -3222,7 +3222,7 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): if name in attrName2Index: attrIndices.append(attrName2Index[name]) del attrName2Index[name] - attrIndices += [attrName2Index[name] for name in sorted(attrName2Index.keys())] + attrIndices += [attrName2Index[name] for name in sorted(attrName2Index)] for index in attrIndices: attr = attrList.item(index) From 103dcc0a48d5d5d09976362cfb86f46c473510ca Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Sun, 8 Apr 2018 15:32:47 +0200 Subject: [PATCH 196/270] Fix handling of boolean flags in elliptical path commands (#183) * properly parse paths without space after boolean flags (fixes #161) * omit space after boolean flag to shave off a few bytes when not using renderer workarounds --- scour/scour.py | 31 ++++++++++++++++++++++------- scour/svg_regex.py | 20 +++++++++++++------ testscour.py | 18 +++++++++++++++++ unittests/path-elliptical-flags.svg | 7 +++++++ 4 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 unittests/path-elliptical-flags.svg diff --git a/scour/scour.py b/scour/scour.py index a1c11a0..753a162 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2580,7 +2580,7 @@ def cleanPolyline(elem, options): def controlPoints(cmd, data): """ - Checks if there are control points in the path + Checks if there are control points in the path data Returns False if there aren't any Returns a list of bools set to True for coordinates in the path data which are control points @@ -2596,13 +2596,28 @@ def controlPoints(cmd, data): return False +def flags(cmd, data): + """ + Checks if there are flags in the path data + + Returns the indices of all values in the path data which are flags + """ + if cmd.lower() == 'a': # a: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ + indices = range(len(data)) + return [index for index in indices if (index % 7) in [3, 4]] + + return [] + + 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, reduce_precision=controlPoints(cmd, data)) + return ''.join([cmd + scourCoordinates(data, options, + reduce_precision=controlPoints(cmd, data), + flags=flags(cmd, data)) for cmd, data in pathObj]) @@ -2614,7 +2629,7 @@ def serializeTransform(transformObj): for command, numbers in transformObj]) -def scourCoordinates(data, options, force_whitespace=False, reduce_precision=False): +def scourCoordinates(data, options, force_whitespace=False, reduce_precision=False, flags=[]): """ Serializes coordinate data with some cleanups: - removes all trailing zeros after the decimal @@ -2636,10 +2651,12 @@ def scourCoordinates(data, options, force_whitespace=False, reduce_precision=Fal # - this number starts with a dot but the previous number had *no* dot or exponent # i.e. '1.3 0.5' -> '1.3.5' or '1e3 0.5' -> '1e3.5' is fine but '123 0.5' -> '123.5' is obviously not # - 'force_whitespace' is explicitly set to 'True' - if c > 0 and (force_whitespace - or scouredCoord[0].isdigit() - or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) - ): + # we never need a space after flags (occuring in elliptical arcs), but librsvg struggles without it + if (c > 0 + and (force_whitespace + or scouredCoord[0].isdigit() + or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord))) + and ((c-1 not in flags) or options.renderer_workaround)): newData.append(' ') # add the scoured coordinate to the path string diff --git a/scour/svg_regex.py b/scour/svg_regex.py index d4dfe3a..c62ba2a 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -247,16 +247,24 @@ class SVGPathParser(object): axis_rotation = Decimal(token[1]) * 1 token = next_val_fn() - if token[1] not in ('0', '1'): + if token[1][0] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) - large_arc_flag = Decimal(token[1]) * 1 + large_arc_flag = Decimal(token[1][0]) * 1 - token = next_val_fn() - if token[1] not in ('0', '1'): + if len(token[1]) > 1: + token = list(token) + token[1] = token[1][1:] + else: + token = next_val_fn() + if token[1][0] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) - sweep_flag = Decimal(token[1]) * 1 + sweep_flag = Decimal(token[1][0]) * 1 - token = next_val_fn() + if len(token[1]) > 1: + token = list(token) + token[1] = token[1][1:] + else: + token = next_val_fn() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = Decimal(token[1]) * 1 diff --git a/testscour.py b/testscour.py index 060b095..b52d98f 100755 --- a/testscour.py +++ b/testscour.py @@ -1101,6 +1101,24 @@ class ChangeQuadToShorthandInPath(unittest.TestCase): 'Did not change quadratic curves into shorthand curve segments in path') +class BooleanFlagsInEllipticalPath(unittest.TestCase): + + def test_omit_spaces(self): + doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--no-renderer-workaround'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths: + self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 00100 50', + 'Did not ommit spaces after boolean flags in elliptical arg path command') + + def test_output_spaces_with_renderer_workaround(self): + doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--renderer-workaround'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths: + self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 0 0 100 50', + 'Did not output spaces after boolean flags in elliptical arg path command ' + 'with renderer workaround') + + class DoNotOptimzePathIfLarger(unittest.TestCase): def runTest(self): diff --git a/unittests/path-elliptical-flags.svg b/unittests/path-elliptical-flags.svg new file mode 100644 index 0000000..cdf13ba --- /dev/null +++ b/unittests/path-elliptical-flags.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg viewBox="-100 -50 300 150" xmlns="http://www.w3.org/2000/svg"> + <path d="m0 0a100 50 0 0 0 100 50" fill="none" stroke="#000"/> + <path d="m0 0a100 50 0 0 0100 50" fill="none" stroke="green"/> + <path d="m0 0a100 50 0 00 100 50" fill="none" stroke="blue"/> + <path d="m0 0a100 50 0 00100 50" fill="none" stroke="red"/> +</svg> From 3283d6d5ecf528425a02edfb5c39a58984ebc45b Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 8 Apr 2018 16:48:33 +0200 Subject: [PATCH 197/270] Simplify control point detection logic - make controlPoints() return a consistent type like flags() - rename the ambiguous "reduce_precision" to "is_control_point" --- scour/scour.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 753a162..7c8d695 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2582,18 +2582,17 @@ def controlPoints(cmd, data): """ Checks if there are control points in the path data - Returns False if there aren't any - Returns a list of bools set to True for coordinates in the path data which are control points + Returns the indices of all values in the path data which are control points """ cmd = cmd.lower() if cmd in ['c', 's', 'q']: indices = range(len(data)) if cmd == 'c': # c: (x1 y1 x2 y2 x y)+ - return [(index % 6) < 4 for index in indices] + return [index for index in indices if (index % 6) < 4] elif cmd in ['s', 'q']: # s: (x2 y2 x y)+ q: (x1 y1 x y)+ - return [(index % 4) < 2 for index in indices] + return [index for index in indices if (index % 4) < 2] - return False + return [] def flags(cmd, data): @@ -2616,7 +2615,7 @@ def serializePath(pathObj, options): # 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, - reduce_precision=controlPoints(cmd, data), + control_points=controlPoints(cmd, data), flags=flags(cmd, data)) for cmd, data in pathObj]) @@ -2629,7 +2628,7 @@ def serializeTransform(transformObj): for command, numbers in transformObj]) -def scourCoordinates(data, options, force_whitespace=False, reduce_precision=False, flags=[]): +def scourCoordinates(data, options, force_whitespace=False, control_points=[], flags=[]): """ Serializes coordinate data with some cleanups: - removes all trailing zeros after the decimal @@ -2642,10 +2641,10 @@ def scourCoordinates(data, options, force_whitespace=False, reduce_precision=Fal c = 0 previousCoord = '' for coord in data: - cp = reduce_precision[c] if isinstance(reduce_precision, list) else reduce_precision + is_control_point = c in control_points scouredCoord = scourUnitlessLength(coord, renderer_workaround=options.renderer_workaround, - reduce_precision=cp) + is_control_point=is_control_point) # don't output a space if this number starts with a dot (.) or minus sign (-); we only need a space if # - this number starts with a digit # - this number starts with a dot but the previous number had *no* dot or exponent @@ -2690,7 +2689,7 @@ def scourLength(length): return scourUnitlessLength(length.value) + Unit.str(length.units) -def scourUnitlessLength(length, renderer_workaround=False, reduce_precision=False): # length is of a numeric type +def scourUnitlessLength(length, renderer_workaround=False, is_control_point=False): # length is of a numeric type """ Scours the numeric part of a length only. Does not accept units. @@ -2703,7 +2702,7 @@ def scourUnitlessLength(length, renderer_workaround=False, reduce_precision=Fals # reduce numeric precision # plus() corresponds to the unary prefix plus operator and applies context precision and rounding - if reduce_precision: + if is_control_point: length = scouringContextC.plus(length) else: length = scouringContext.plus(length) From 0254014e06e821c7c2b7cf888e220d42279a807e Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Fri, 13 Apr 2018 20:01:50 +0000 Subject: [PATCH 198/270] Enable shortenIDs to recycle existing IDs This patch enables shortenIDs to remap IDs currently in use. This is very helpful to ensure that scour does not change been "optimal" and "suboptimal" choices for IDs as observed in GH#186. Closes: #186 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 81 ++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 7c8d695..1f8780a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -690,19 +690,16 @@ def removeUnreferencedElements(doc, keepDefs): return num -def shortenIDs(doc, prefix, unprotectedElements=None): +def shortenIDs(doc, prefix, options): """ 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 identifiedElements = findElementsWithId(doc.documentElement) - if unprotectedElements is None: - unprotectedElements = identifiedElements referencedIDs = findReferencedElements(doc.documentElement) # Make idList (list of idnames) sorted by reference count @@ -710,24 +707,28 @@ def shortenIDs(doc, prefix, unprotectedElements=None): # 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 = [(len(referencedIDs[rid]), rid) for rid in referencedIDs - if rid in unprotectedElements] + if rid in identifiedElements] 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 rid not in idList]) + idList.extend([rid for rid in identifiedElements if rid not in idList]) + # Ensure we do not reuse a protected ID by accident + protectedIDs = _protectedIDs(identifiedElements, options) 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. + + # Skip ahead if the new ID is in use and protected. + while curId in protectedIDs: + curIdNum += 1 + curId = intToID(curIdNum, prefix) + + # Now check if we found a new ID (we can happen to choose the same + # ID. More likely on a rerun) 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 @@ -755,7 +756,7 @@ 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. - Updates identifiedElements and referencedIDs. + Updates identifiedElements, but not referencedIDs. Does not handle the case where idTo is already the ID name of another element in doc. @@ -822,34 +823,44 @@ def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): node.setAttribute(attr, newValue) num += len(oldValue) - len(newValue) - del referencedIDs[idFrom] - referencedIDs[idTo] = referringNodes + # We deliberately leave referencedIDs alone to enable us to bulk update + # IDs where two nodes swap IDs. (GH#186) + # del referencedIDs[idFrom] + # referencedIDs[idTo] = referringNodes return num +def _protectedIDs(seenIDs, options): + """Return a list of protected IDs out of the seenIDs""" + protectedIDs = [] + if options.protect_ids_prefix or options.protect_ids_noninkscape or options.protect_ids_list: + protect_ids_prefixes = [] + protect_ids_list = [] + 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 seenIDs: + protected = False + if options.protect_ids_noninkscape and not id[-1].isdigit(): + protected = True + elif protect_ids_list and id in protect_ids_list: + protected = True + elif protect_ids_prefixes: + if any(id.startswith(prefix) for prefix in protect_ids_prefixes): + protected = True + if protected: + protectedIDs.append(id) + return protectedIDs + + 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): - 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: + protectedIDs = _protectedIDs(identifiedElements, options) + if protectedIDs: + for id in protectedIDs: del identifiedElements[id] return identifiedElements @@ -3527,7 +3538,7 @@ def scourString(in_string, options=None): # shorten ID names as much as possible if options.shorten_ids: - _num_bytes_saved_in_ids += shortenIDs(doc, options.shorten_ids_prefix, unprotected_ids(doc, options)) + _num_bytes_saved_in_ids += shortenIDs(doc, options.shorten_ids_prefix, options) # scour lengths (including coordinates) for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', From 00cf42b554e11e09348218b9e66d72de178694ae Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 15 Apr 2018 16:22:00 +0000 Subject: [PATCH 199/270] Rename function to match DEP8 conventions Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1f8780a..ad675e6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -714,7 +714,7 @@ def shortenIDs(doc, prefix, options): # Add unreferenced IDs to end of idList in arbitrary order idList.extend([rid for rid in identifiedElements if rid not in idList]) # Ensure we do not reuse a protected ID by accident - protectedIDs = _protectedIDs(identifiedElements, options) + protectedIDs = protected_ids(identifiedElements, options) curIdNum = 1 @@ -831,7 +831,7 @@ def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): return num -def _protectedIDs(seenIDs, options): +def protected_ids(seenIDs, options): """Return a list of protected IDs out of the seenIDs""" protectedIDs = [] if options.protect_ids_prefix or options.protect_ids_noninkscape or options.protect_ids_list: @@ -858,7 +858,7 @@ def _protectedIDs(seenIDs, options): def unprotected_ids(doc, options): u"""Returns a list of unprotected IDs within the document doc.""" identifiedElements = findElementsWithId(doc.documentElement) - protectedIDs = _protectedIDs(identifiedElements, options) + protectedIDs = protected_ids(identifiedElements, options) if protectedIDs: for id in protectedIDs: del identifiedElements[id] From 5dc1b7a820b57801fa3db03510aad719c34ce674 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 10 Apr 2018 05:29:48 +0000 Subject: [PATCH 200/270] scour: Make optimized default_attribute data structures There are a lot of "DefaultAttribute"s and for a given tag, most of the "DefaultAttribute"s are not applicable. Therefore, we create two data structures to assist us with only dealing with the attributes that matter. Here there are two cases: * Those that always matter. These go into default_attributes_unrestricted list. * Those that matter only based on the node name. These go into the default_attributes_restricted_by_tag with the node name as key (with the value being a list of matching attributes). In the next commit, we will use those for optimizing the removal of default attributes. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index 7c8d695..23d0574 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1866,6 +1866,20 @@ default_attributes = [ DefaultAttribute('yChannelSelector', 'A', elements='feDisplacementMap') ] +default_attributes_restricted_by_tag = defaultdict(list) +default_attributes_unrestricted = [] + +for attr in default_attributes: + if attr.elements is None: + # Applies to all tags + default_attributes_unrestricted.append(attr) + continue + if type(attr.elements) is str: + default_attributes_restricted_by_tag[attr.elements].append(attr) + else: + for tag in attr.elements: + default_attributes_restricted_by_tag[tag].append(attr) + def taint(taintedSet, taintedAttribute): u"""Adds an attribute to a set of attributes. From 1650f91ea41f728e67b70317619c90bbcd8194b5 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 10 Apr 2018 05:53:21 +0000 Subject: [PATCH 201/270] Optimize removeDefaultAttributeValues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid looping over DefaultAttribute(s) that are not relevant for a given node. This skips a lot of calls to removeDefaultAttributeValue but more importantly, it avoids "node.nodeName not in attribute.elements" line in removeDefaultAttributeValue. As attribute.elements is a list, this becomes expensive for "larger lists" (or in this case when there are a lot of attributes). This seems to remove about 1½-2 minutes of runtime (out of ~8) on the 1_42_polytope_7-cube.svg test case provided in #184. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 23d0574..fb1ce9b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1900,9 +1900,6 @@ def removeDefaultAttributeValue(node, attribute): if not node.hasAttribute(attribute.name): 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: @@ -1931,9 +1928,17 @@ def removeDefaultAttributeValues(node, options, tainted=set()): if node.nodeType != Node.ELEMENT_NODE: return 0 - # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) - for attribute in default_attributes: + # Remove all default attributes. The remoteDefaultAttributeValue + # function deals with "if/when" we are allowed to remove the + # attribute as long as we supply it only with attributes that are + # applicable for this given node. That part is handled by using + # default_attributes_unrestricted and + # default_attributes_restricted_by_tag + for attribute in default_attributes_unrestricted: num += removeDefaultAttributeValue(node, attribute) + if node.nodeName in default_attributes_restricted_by_tag: + for attribute in default_attributes_restricted_by_tag[node.nodeName]: + num += removeDefaultAttributeValue(node, attribute) # Summarily get rid of default properties attributes = [node.attributes.item(i).nodeName for i in range(node.attributes.length)] From 20dcbcbe64a17fea60b2ca9ea490712daf2600b8 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 15 Apr 2018 18:28:32 +0200 Subject: [PATCH 202/270] 'default_attributes': make sure 'elements' is a list --- scour/scour.py | 126 ++++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index fb1ce9b..f7177c4 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1741,14 +1741,14 @@ DefaultAttribute = namedtuple('DefaultAttribute', ['name', 'value', 'units', 'el DefaultAttribute.__new__.__defaults__ = (None,) * len(DefaultAttribute._fields) default_attributes = [ # unit systems - DefaultAttribute('clipPathUnits', 'userSpaceOnUse', elements='clipPath'), - DefaultAttribute('filterUnits', 'objectBoundingBox', elements='filter'), + 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('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', @@ -1759,9 +1759,9 @@ default_attributes = [ 'use', 'view']), # svg elements - DefaultAttribute('width', 100, Unit.PCT, elements='svg'), - DefaultAttribute('height', 100, Unit.PCT, elements='svg'), - DefaultAttribute('baseProfile', 'none', elements='svg'), + 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']), @@ -1771,26 +1771,26 @@ default_attributes = [ 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('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'), + 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'), + 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']), @@ -1807,63 +1807,63 @@ default_attributes = [ 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', + DefaultAttribute('x1', 0, elements=['linearGradient']), + DefaultAttribute('y1', 0, elements=['linearGradient']), + DefaultAttribute('y2', 0, elements=['linearGradient']), + DefaultAttribute('x2', 100, Unit.PCT, elements=['linearGradient']), + DefaultAttribute('x2', 1, Unit.NONE, elements=['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', + DefaultAttribute('fx', elements=['radialGradient'], conditions=lambda node: node.getAttribute('fx') == node.getAttribute('cx')), - DefaultAttribute('fy', elements='radialGradient', + 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', + DefaultAttribute('r', 50, Unit.PCT, elements=['radialGradient']), + DefaultAttribute('r', 0.5, Unit.NONE, elements=['radialGradient'], conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('cx', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('cx', 0.5, Unit.NONE, 'radialGradient', + DefaultAttribute('cx', 50, Unit.PCT, elements=['radialGradient']), + DefaultAttribute('cx', 0.5, Unit.NONE, elements=['radialGradient'], conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('cy', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('cy', 0.5, Unit.NONE, 'radialGradient', + DefaultAttribute('cy', 50, Unit.PCT, elements=['radialGradient']), + DefaultAttribute('cy', 0.5, Unit.NONE, elements=['radialGradient'], conditions=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('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('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('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('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('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') + DefaultAttribute('type', 'matrix', elements=['feColorMatrix']), + DefaultAttribute('type', 'turbulence', elements=['feTurbulence']), + DefaultAttribute('xChannelSelector', 'A', elements=['feDisplacementMap']), + DefaultAttribute('yChannelSelector', 'A', elements=['feDisplacementMap']) ] default_attributes_restricted_by_tag = defaultdict(list) From 0ec0732447dc82e899dc17dcbb55fc285bcfb8b0 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 15 Apr 2018 18:33:46 +0200 Subject: [PATCH 203/270] Simplify 'default_attributes' handling a bit --- scour/scour.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index f7177c4..93d3f81 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1866,19 +1866,16 @@ default_attributes = [ DefaultAttribute('yChannelSelector', 'A', elements=['feDisplacementMap']) ] -default_attributes_restricted_by_tag = defaultdict(list) -default_attributes_unrestricted = [] - -for attr in default_attributes: - if attr.elements is None: - # Applies to all tags - default_attributes_unrestricted.append(attr) - continue - if type(attr.elements) is str: - default_attributes_restricted_by_tag[attr.elements].append(attr) +# split to increase lookup performance +default_attributes_universal = [] # list containing attributes valid for all elements +default_attributes_per_element = defaultdict(list) # dict containing lists of attributes valid for individual elements +for default_attribute in default_attributes: + if default_attribute.elements is None: + default_attributes_universal.append(default_attribute) else: - for tag in attr.elements: - default_attributes_restricted_by_tag[tag].append(attr) + for element in default_attribute.elements: + default_attributes_per_element[element].append(default_attribute) +print(len(default_attributes_universal)) def taint(taintedSet, taintedAttribute): @@ -1896,6 +1893,8 @@ def taint(taintedSet, taintedAttribute): def removeDefaultAttributeValue(node, attribute): """ Removes the DefaultAttribute 'attribute' from 'node' if specified conditions are fulfilled + + Warning: Does NOT check if the attribute is actually valid for the passed element type for increased preformance! """ if not node.hasAttribute(attribute.name): return 0 @@ -1928,16 +1927,15 @@ def removeDefaultAttributeValues(node, options, tainted=set()): if node.nodeType != Node.ELEMENT_NODE: return 0 - # Remove all default attributes. The remoteDefaultAttributeValue - # function deals with "if/when" we are allowed to remove the - # attribute as long as we supply it only with attributes that are - # applicable for this given node. That part is handled by using - # default_attributes_unrestricted and - # default_attributes_restricted_by_tag - for attribute in default_attributes_unrestricted: + # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) + # + # For increased performance do not iterate the whole list for each element but run only on valid subsets + # - 'default_attributes_universal' (attributes valid for all elements) + # - 'default_attributes_per_element' (attributes specific to one specific element type) + for attribute in default_attributes_universal: num += removeDefaultAttributeValue(node, attribute) - if node.nodeName in default_attributes_restricted_by_tag: - for attribute in default_attributes_restricted_by_tag[node.nodeName]: + if node.nodeName in default_attributes_per_element: + for attribute in default_attributes_per_element[node.nodeName]: num += removeDefaultAttributeValue(node, attribute) # Summarily get rid of default properties From 8ddb7d8913f862006cdc99d82dcb97e4f209f418 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 15 Apr 2018 18:40:06 +0200 Subject: [PATCH 204/270] Add valid elements for 'spreadMethod' attribute Turns out 'default_attributes_universal' is actually empty right now so we might consider removing it altogether... --- scour/scour.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 93d3f81..c6f4c75 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1827,7 +1827,7 @@ default_attributes = [ DefaultAttribute('cy', 50, Unit.PCT, elements=['radialGradient']), DefaultAttribute('cy', 0.5, Unit.NONE, elements=['radialGradient'], conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('spreadMethod', 'pad'), + DefaultAttribute('spreadMethod', 'pad', elements=['linearGradient', 'radialGradient']), # filter effects DefaultAttribute('amplitude', 1, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), @@ -1867,6 +1867,7 @@ default_attributes = [ ] # split to increase lookup performance +# TODO: 'default_attributes_universal' is actually empty right now - will we ever need it? default_attributes_universal = [] # list containing attributes valid for all elements default_attributes_per_element = defaultdict(list) # dict containing lists of attributes valid for individual elements for default_attribute in default_attributes: @@ -1875,7 +1876,6 @@ for default_attribute in default_attributes: else: for element in default_attribute.elements: default_attributes_per_element[element].append(default_attribute) -print(len(default_attributes_universal)) def taint(taintedSet, taintedAttribute): @@ -1893,7 +1893,7 @@ def taint(taintedSet, taintedAttribute): def removeDefaultAttributeValue(node, attribute): """ Removes the DefaultAttribute 'attribute' from 'node' if specified conditions are fulfilled - + Warning: Does NOT check if the attribute is actually valid for the passed element type for increased preformance! """ if not node.hasAttribute(attribute.name): From d6406a34706e7ae1422595a5eda6d8bc7b50b1a8 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 15 Apr 2018 16:29:44 +0000 Subject: [PATCH 205/270] shortenIDs: Avoid pointless renames of IDs With the current code, scour could do a pointless remap of an ID, where there is no benefit in it. Consider: ```xml <?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <rect id="a" width="80" height="50" fill="red"/> <rect id="b" width="80" height="50" fill="blue"/> </defs> <use xlink:href="#a"/> <use xlink:href="#b"/> <use xlink:href="#b"/> </svg> ``` In this example, there is no point in swapping the IDs - even if "#b" is used more often than "#a", they have the same length. Besides a performance win on an already scour'ed image, it also mean scour will behave like a function with a fixed-point (i.e. scour eventually stops altering the image). To solve this, we no longer check whether an we find exactly the same ID. Instead, we look at the length of the new ID compared to the original. This gives us a slight complication as we can now "reserve" a "future" ID to avoid the rename. Thanks to Eduard "Ede_123" Braun for providing the test case. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 24 +++++++++++++++++++----- testscour.py | 13 +++++++++++++ unittests/shorten-ids-stable-output.svg | 11 +++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 unittests/shorten-ids-stable-output.svg diff --git a/scour/scour.py b/scour/scour.py index ad675e6..95e24b5 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -715,22 +715,36 @@ def shortenIDs(doc, prefix, options): idList.extend([rid for rid in identifiedElements if rid not in idList]) # Ensure we do not reuse a protected ID by accident protectedIDs = protected_ids(identifiedElements, options) + # IDs that we have used "ahead of time". Happens if we are about to + # rename an ID and there is no length difference between the new and + # the old ID. + consumedIDs = set() curIdNum = 1 for rid in idList: curId = intToID(curIdNum, prefix) - # Skip ahead if the new ID is in use and protected. - while curId in protectedIDs: + # Skip ahead if the new ID has already been used or is protected. + while curId in protectedIDs or curId in consumedIDs: curIdNum += 1 curId = intToID(curIdNum, prefix) - # Now check if we found a new ID (we can happen to choose the same - # ID. More likely on a rerun) - if curId != rid: + # Avoid checking the ID if it will not affect the size of the document + # (e.g. remapping "c" to "a" is not going to win us anything) + if len(curId) != len(rid): # Then go rename it. num += renameID(doc, rid, curId, identifiedElements, referencedIDs) + elif curId < rid: + # If we skip reassigning an ID because it has the same length + # (E.g. skipping "c" -> "a"), then we have to mark the "future" + # ID as taken ("c" in the example). + # The "strictly less than" in the condition is to ensure that we + # properly update curIdNum in the corner case where curId == rid. + consumedIDs.add(rid) + # Use continue here without updating curIdNum to avoid losing + # the current ID. + continue curIdNum += 1 return num diff --git a/testscour.py b/testscour.py index b52d98f..eb62af7 100755 --- a/testscour.py +++ b/testscour.py @@ -1948,6 +1948,19 @@ class ShortenIDsOption(unittest.TestCase): 'Did not update reference to shortened ID') +class ShortenIDsStableOutput(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/shorten-ids-stable-output.svg', + parse_args(['--shorten-ids'])) + use_tags = doc.getElementsByTagName('use') + hrefs_ordered = [x.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + for x in use_tags] + expected = ['#a', '#b', '#b'] + self.assertEquals(hrefs_ordered, expected, + '--shorten-ids pointlessly reassigned ids') + + class MustKeepGInSwitch(unittest.TestCase): def runTest(self): diff --git a/unittests/shorten-ids-stable-output.svg b/unittests/shorten-ids-stable-output.svg new file mode 100644 index 0000000..f2df1bc --- /dev/null +++ b/unittests/shorten-ids-stable-output.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <rect id="a" width="80" height="50" fill="red"/> + <rect id="b" width="80" height="50" fill="blue"/> + </defs> + <use xlink:href="#a"/> + <use xlink:href="#b"/> + <use xlink:href="#b"/> +</svg> + From 91503c6d7e0882ebfff7bb52def7938a63e9864e Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 15 Apr 2018 17:04:38 +0000 Subject: [PATCH 206/270] renameID: Replace referencedIDs with referringNodes This change pushes the responsibility of updating referencedIDs to its callers where needed. The only caller of renameIDs is shortenIDs and that works perfectly fine without updating its copy of referencedIDs. In shortenIDs, we need to be able to lookup which nodes referenced the "original ID" (and not the "new ID"). While shortenIDs *could* update referencedIDs so it remained valid, it is extra complexity for no gain. As an example of this complexity, imagine if two or more IDs are "rotated" like so: Original IDs: a, bb, ccc, dddd Mapping: dddd -> ccc ccc -> bb bb -> a a -> dddd While doable within reasonable performance, we do not need to support it at the moment, so there is no reason to handle that complexity. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 95e24b5..d74d3d2 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -700,6 +700,10 @@ def shortenIDs(doc, prefix, options): num = 0 identifiedElements = findElementsWithId(doc.documentElement) + # This map contains maps the (original) ID to the nodes referencing it. + # At the end of this function, it will no longer be valid and while we + # could keep it up to date, it will complicate the code for no gain + # (as we do not reuse the data structure beyond this function). referencedIDs = findReferencedElements(doc.documentElement) # Make idList (list of idnames) sorted by reference count @@ -734,7 +738,7 @@ def shortenIDs(doc, prefix, options): # (e.g. remapping "c" to "a" is not going to win us anything) if len(curId) != len(rid): # Then go rename it. - num += renameID(doc, rid, curId, identifiedElements, referencedIDs) + num += renameID(doc, rid, curId, identifiedElements, referencedIDs.get(rid)) elif curId < rid: # If we skip reassigning an ID because it has the same length # (E.g. skipping "c" -> "a"), then we have to mark the "future" @@ -765,14 +769,12 @@ def intToID(idnum, prefix): return prefix + rid -def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): +def renameID(doc, idFrom, idTo, identifiedElements, referringNodes): """ Changes the ID name from idFrom to idTo, on the declaring element - as well as all references in the document doc. + as well as all nodes in referringNodes. - Updates identifiedElements, but not referencedIDs. - Does not handle the case where idTo is already the ID name - of another element in doc. + Updates identifiedElements. Returns the number of bytes saved by this replacement. """ @@ -786,7 +788,6 @@ def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): num += len(idFrom) - len(idTo) # 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, @@ -837,11 +838,6 @@ def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): node.setAttribute(attr, newValue) num += len(oldValue) - len(newValue) - # We deliberately leave referencedIDs alone to enable us to bulk update - # IDs where two nodes swap IDs. (GH#186) - # del referencedIDs[idFrom] - # referencedIDs[idTo] = referringNodes - return num From e25b0dae73a1fb7fa3dbe09369d3e9aacefb359f Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 15 Apr 2018 17:36:07 +0000 Subject: [PATCH 207/270] Remove a (now) unused parameter to renameID Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index d74d3d2..ad5c7d4 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -738,7 +738,7 @@ def shortenIDs(doc, prefix, options): # (e.g. remapping "c" to "a" is not going to win us anything) if len(curId) != len(rid): # Then go rename it. - num += renameID(doc, rid, curId, identifiedElements, referencedIDs.get(rid)) + num += renameID(rid, curId, identifiedElements, referencedIDs.get(rid)) elif curId < rid: # If we skip reassigning an ID because it has the same length # (E.g. skipping "c" -> "a"), then we have to mark the "future" @@ -769,7 +769,7 @@ def intToID(idnum, prefix): return prefix + rid -def renameID(doc, idFrom, idTo, identifiedElements, referringNodes): +def renameID(idFrom, idTo, identifiedElements, referringNodes): """ Changes the ID name from idFrom to idTo, on the declaring element as well as all nodes in referringNodes. From 039022ee9d04b8a3b9cd5e0e25223f15a6a2fbf6 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Mon, 16 Apr 2018 18:49:27 +0000 Subject: [PATCH 208/270] shortenID: Improve tracking of optimal ID lengths Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 94 +++++++++++++++++++------ unittests/shorten-ids-stable-output.svg | 6 +- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index ad5c7d4..1e7316c 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -719,41 +719,91 @@ def shortenIDs(doc, prefix, options): idList.extend([rid for rid in identifiedElements if rid not in idList]) # Ensure we do not reuse a protected ID by accident protectedIDs = protected_ids(identifiedElements, options) - # IDs that we have used "ahead of time". Happens if we are about to - # rename an ID and there is no length difference between the new and - # the old ID. + # IDs that have been allocated and should not be remapped. consumedIDs = set() + # List of IDs that need to be assigned a new ID. The list is ordered + # such that earlier entries will be assigned a shorter ID than those + # later in the list. IDs in this list *can* obtain an ID that is + # longer than they already are. + need_new_id = [] + + id_allocations = list(compute_id_lengths(len(idList) + 1)) + # Reverse so we can use it as a stack and still work from "shortest to + # longest" ID. + id_allocations.reverse() + + # Here we loop over all current IDs (that we /might/ want to remap) + # and group them into two. 1) The IDs that already have a perfect + # length (these are added to consumedIDs) and 2) the IDs that need + # to change length (these are appended to need_new_id). + optimal_id_length, id_use_limit = 0, 0 + for current_id in idList: + # If we are out of IDs of the current length, then move on + # to the next length + if id_use_limit < 1: + optimal_id_length, id_use_limit = id_allocations.pop() + # Reserve an ID from this length + id_use_limit -= 1 + # We check for strictly equal to optimal length because our ID + # remapping may have to assign one node a longer ID because + # another node needs a shorter ID. + if len(current_id) == optimal_id_length: + # This rid is already of optimal length - lets just keep it. + consumedIDs.add(current_id) + else: + # Needs a new (possibly longer) ID. + need_new_id.append(current_id) + curIdNum = 1 - for rid in idList: - curId = intToID(curIdNum, prefix) + for old_id in need_new_id: + new_id = intToID(curIdNum, prefix) # Skip ahead if the new ID has already been used or is protected. - while curId in protectedIDs or curId in consumedIDs: + while new_id in protectedIDs or new_id in consumedIDs: curIdNum += 1 - curId = intToID(curIdNum, prefix) + new_id = intToID(curIdNum, prefix) - # Avoid checking the ID if it will not affect the size of the document - # (e.g. remapping "c" to "a" is not going to win us anything) - if len(curId) != len(rid): - # Then go rename it. - num += renameID(rid, curId, identifiedElements, referencedIDs.get(rid)) - elif curId < rid: - # If we skip reassigning an ID because it has the same length - # (E.g. skipping "c" -> "a"), then we have to mark the "future" - # ID as taken ("c" in the example). - # The "strictly less than" in the condition is to ensure that we - # properly update curIdNum in the corner case where curId == rid. - consumedIDs.add(rid) - # Use continue here without updating curIdNum to avoid losing - # the current ID. - continue + # Now that we have found the first available ID, do the remap. + num += renameID(old_id, new_id, identifiedElements, referencedIDs.get(old_id)) curIdNum += 1 return num +def compute_id_lengths(highest): + """Compute how many IDs are available of a given size + + Example: + >>> lengths = list(compute_id_lengths(512)) + >>> lengths + [(1, 26), (2, 676)] + >>> total_limit = sum(x[1] for x in lengths) + >>> total_limit + 702 + >>> intToID(total_limit, '') + 'zz' + + Which tells us that we got 26 IDs of length 1 and up to 676 IDs of length two + if we need to allocate 512 IDs. + + :param highest: Highest ID that need to be allocated + :return: An iterator that returns tuples of (id-length, use-limit). The + use-limit applies only to the given id-length (i.e. it is excluding IDs + of shorter length). Note that the sum of the use-limit values is always + equal to or greater than the highest param. + """ + step = 26 + id_length = 0 + use_limit = 1 + while highest: + id_length += 1 + use_limit *= step + yield (id_length, use_limit) + highest = int((highest - 1) / step) + + def intToID(idnum, prefix): """ Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, diff --git a/unittests/shorten-ids-stable-output.svg b/unittests/shorten-ids-stable-output.svg index f2df1bc..6905ec1 100644 --- a/unittests/shorten-ids-stable-output.svg +++ b/unittests/shorten-ids-stable-output.svg @@ -2,10 +2,10 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <rect id="a" width="80" height="50" fill="red"/> - <rect id="b" width="80" height="50" fill="blue"/> + <rect id="long_id" width="80" height="50" fill="blue"/> </defs> <use xlink:href="#a"/> - <use xlink:href="#b"/> - <use xlink:href="#b"/> + <use xlink:href="#long_id"/> + <use xlink:href="#long_id"/> </svg> From a459d629c1e4ea3e18c3480021e9ea0b32629ab3 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 17 Apr 2018 19:05:52 +0000 Subject: [PATCH 209/270] removeDefaultAttributeValue: Special-case order attribute Scour tried to handle "order" attribute as a SVGLength. However, the "order" attribute *can* consist of two integers according to the [SVG 1.1 Specification] and SVGLength is not designed to handle that. With this change, we now pretend that "order" is a string, which side steps this issue. [SVG 1.1 Specification]: https://www.w3.org/TR/SVG11/single-page.html#filters-feConvolveMatrixElementOrderAttribute Closes: #189 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 7 ++++++- testscour.py | 10 ++++++++++ unittests/remove-default-attr-order.svg | 11 +++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 unittests/remove-default-attr-order.svg diff --git a/scour/scour.py b/scour/scour.py index c6f4c75..e5346b8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1848,7 +1848,12 @@ default_attributes = [ DefaultAttribute('offset', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), DefaultAttribute('operator', 'over', elements=['feComposite']), DefaultAttribute('operator', 'erode', elements=['feMorphology']), - DefaultAttribute('order', 3, elements=['feConvolveMatrix']), + # We pretend order is a string (because handling it as an + # SVGLength will cause issues when order is two integers). Note + # that order must be exactly one or two integers (no units or + # fancy numbers), so working with it a string will generally just + # work. + DefaultAttribute('order', '3', elements=['feConvolveMatrix']), DefaultAttribute('pointsAtX', 0, elements=['feSpotLight']), DefaultAttribute('pointsAtY', 0, elements=['feSpotLight']), DefaultAttribute('pointsAtZ', 0, elements=['feSpotLight']), diff --git a/testscour.py b/testscour.py index b52d98f..2c25258 100755 --- a/testscour.py +++ b/testscour.py @@ -1570,6 +1570,16 @@ class RemoveDefaultGradFYValue(unittest.TestCase): 'fy matching cy not removed') +class RemoveDefaultAttributeOrderSVGLengthCrash(unittest.TestCase): + + # Triggered a crash in v0.36 + def runTest(self): + try: + scourXmlFile('unittests/remove-default-attr-order.svg') + except AttributeError: + self.fail("Processing the order attribute triggered an AttributeError ") + + class CDATAInXml(unittest.TestCase): def runTest(self): diff --git a/unittests/remove-default-attr-order.svg b/unittests/remove-default-attr-order.svg new file mode 100644 index 0000000..d65848a --- /dev/null +++ b/unittests/remove-default-attr-order.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink"> + <defs> + <filter id="filter" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%"> + <feConvolveMatrix order="3 1" kernelMatrix="0.3333 0.3333 0.3333" edgeMode="none"/> + </filter> + </defs> + <!-- Use the filter (otherwise, scour discards it before it trips over it) --> + <image id="png" x="10" y="30" width="150" height="50" xlink:href="raster.png" + filter="url(#filter)"/> +</svg> From 18e57cddaef422b8bac3875d7d09d9fbac9800bc Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Wed, 18 Apr 2018 05:40:59 +0000 Subject: [PATCH 210/270] Avoid crashing on "scale(1)" (short for "scale(1, 1)") The scale function on the transform attribute has a short form, where only the first argument is used. But optimizeTransform would always assume that there were two when checking for the identity scale. Closes: #190 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 4 +++- testscour.py | 11 +++++++++++ unittests/transform-scale-is-identity.svg | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 unittests/transform-scale-is-identity.svg diff --git a/scour/scour.py b/scour/scour.py index c6f4c75..9b15637 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2966,7 +2966,9 @@ def optimizeTransform(transform): # y1 * uniformscalefactor2 prevArgs[1] *= currArgs[0] del transform[i] - if prevArgs[0] == prevArgs[1] == 1: + # if prevArgs is [1] or [1, 1], then it is effectively an + # identity matrix and can be removed. + if prevArgs[0] == 1 and (len(prevArgs) == 1 or prevArgs[1] == 1): # Identity scale! i -= 1 del transform[i] diff --git a/testscour.py b/testscour.py index b52d98f..895a28d 100755 --- a/testscour.py +++ b/testscour.py @@ -2304,6 +2304,17 @@ class TransformIdentityTranslate(unittest.TestCase): 'Transform containing identity translation not removed') +class TransformIdentityScale(unittest.TestCase): + + def runTest(self): + try: + doc = scourXmlFile('unittests/transform-scale-is-identity.svg') + except IndexError: + self.fail("scour failed to handled scale(1) [See GH#190]") + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('scale'), '', + 'Transform containing identity translation not removed') + + class DuplicateGradientsUpdateStyle(unittest.TestCase): def runTest(self): diff --git a/unittests/transform-scale-is-identity.svg b/unittests/transform-scale-is-identity.svg new file mode 100644 index 0000000..037d38a --- /dev/null +++ b/unittests/transform-scale-is-identity.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-9 0 9 9"> + <line stroke="rgba(255,0,0,0.5)" y1="9" x1="9" transform="scale(1 1) scale(1)"/> + <line stroke="rgba(0,0,255,0.5)" y1="9" x1="9"/> +</svg> From c504891bd74044297fc82d318b8b2b5c197e13f1 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 21 Apr 2018 06:19:38 +0000 Subject: [PATCH 211/270] test: Use number-optional-number variant of kernelUnitLength Signed-off-by: Niels Thykier <niels@thykier.net> --- unittests/remove-default-attr-order.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittests/remove-default-attr-order.svg b/unittests/remove-default-attr-order.svg index d65848a..506c9ce 100644 --- a/unittests/remove-default-attr-order.svg +++ b/unittests/remove-default-attr-order.svg @@ -2,7 +2,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink"> <defs> <filter id="filter" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%"> - <feConvolveMatrix order="3 1" kernelMatrix="0.3333 0.3333 0.3333" edgeMode="none"/> + <feConvolveMatrix order="3 1" kernelMatrix="0.3333 0.3333 0.3333" kernelUnitLength="3 1" edgeMode="none"/> </filter> </defs> <!-- Use the filter (otherwise, scour discards it before it trips over it) --> From 8a2892b458a385dedcd19e21d53d136d467efb0f Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 21 Apr 2018 06:38:28 +0000 Subject: [PATCH 212/270] Avoid crashing on stdDeviation attribute Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 3 ++- testscour.py | 12 +++++++++++- unittests/remove-default-attr-std-deviation.svg | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 unittests/remove-default-attr-std-deviation.svg diff --git a/scour/scour.py b/scour/scour.py index e5346b8..3e5a8e5 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1862,7 +1862,8 @@ default_attributes = [ DefaultAttribute('seed', 0, elements=['feTurbulence']), DefaultAttribute('specularConstant', 1, elements=['feSpecularLighting']), DefaultAttribute('specularExponent', 1, elements=['feSpecularLighting', 'feSpotLight']), - DefaultAttribute('stdDeviation', 0, elements=['feGaussianBlur']), + # Pretend it is a string (for the same reasons as we do with "order") + DefaultAttribute('stdDeviation', '0', elements=['feGaussianBlur']), DefaultAttribute('stitchTiles', 'noStitch', elements=['feTurbulence']), DefaultAttribute('surfaceScale', 1, elements=['feDiffuseLighting', 'feSpecularLighting']), DefaultAttribute('type', 'matrix', elements=['feColorMatrix']), diff --git a/testscour.py b/testscour.py index 2c25258..c182a35 100755 --- a/testscour.py +++ b/testscour.py @@ -1577,7 +1577,17 @@ class RemoveDefaultAttributeOrderSVGLengthCrash(unittest.TestCase): try: scourXmlFile('unittests/remove-default-attr-order.svg') except AttributeError: - self.fail("Processing the order attribute triggered an AttributeError ") + self.fail("Processing the order attribute triggered an AttributeError") + + +class RemoveDefaultAttributeStdDeviationSVGLengthCrash(unittest.TestCase): + + # Triggered a crash in v0.36 + def runTest(self): + try: + scourXmlFile('unittests/remove-default-attr-std-deviation.svg') + except AttributeError: + self.fail("Processing the order attribute triggered an AttributeError") class CDATAInXml(unittest.TestCase): diff --git a/unittests/remove-default-attr-std-deviation.svg b/unittests/remove-default-attr-std-deviation.svg new file mode 100644 index 0000000..ba88368 --- /dev/null +++ b/unittests/remove-default-attr-std-deviation.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink"> + <defs> + <filter id="filter" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%"> + <feGaussianBlur stdDeviation="0 0" x="10" y="10" dx="20" dy="20" /> + </filter> + </defs> + <!-- Use the filter (otherwise, scour discards it before it trips over it) --> + <image id="png" x="10" y="30" width="150" height="50" xlink:href="raster.png" + filter="url(#filter)"/> +</svg> From 3c64623a127ec8297d5161382259431397725682 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Fri, 29 Jun 2018 19:29:09 +0200 Subject: [PATCH 213/270] Discontinue official support for Python 3.3 (testing failed due to wheel now requiring Python >= 3.4) Also run flake8 in latest Python 3.6 (3.7 is not supported on Travis yet) --- .travis.yml | 3 +-- README.md | 2 +- tox.ini | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5200f17..8f937d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ language: python python: - pypy - 2.7 - - 3.3 - 3.4 - 3.5 - 3.6 @@ -19,7 +18,7 @@ matrix: fast_finish: true include: - - python: 3.5 + - python: 3.6 env: - TOXENV=flake8 diff --git a/README.md b/README.md index fd8c324..3eb1529 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The project moved to GitLab in 2013 an is now maintained by Tobias "oberstet" Ob ## Installation -Scour requires [Python](https://www.python.org) 2.7 or 3.3+. Further, for installation, [pip](https://pip.pypa.io) should be used. +Scour requires [Python](https://www.python.org) 2.7 or 3.4+. Further, for installation, [pip](https://pip.pypa.io) should be used. To install the [latest release](https://pypi.python.org/pypi/scour) of Scour from PyPI: diff --git a/tox.ini b/tox.ini index df04c05..5c67f5b 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ envlist = pypy py27 - py33 py34 py35 py36 From 5d579f8927c426e212e96457c3e8e23870555932 Mon Sep 17 00:00:00 2001 From: Patrick Storz <Eduard.Braun2@gmx.de> Date: Sat, 30 Jun 2018 18:58:36 +0200 Subject: [PATCH 214/270] Also special-case baseFrequency and add 'radius --- scour/scour.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 3e5a8e5..b656e12 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1830,9 +1830,12 @@ default_attributes = [ DefaultAttribute('spreadMethod', 'pad', elements=['linearGradient', 'radialGradient']), # filter effects + # TODO: Some numerical attributes allow an optional second value ("number-optional-number") + # and are currently handled as strings to avoid an exception in 'SVGLength', see + # https://github.com/scour-project/scour/pull/192 DefaultAttribute('amplitude', 1, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), DefaultAttribute('azimuth', 0, elements=['feDistantLight']), - DefaultAttribute('baseFrequency', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), + DefaultAttribute('baseFrequency', '0', elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), DefaultAttribute('bias', 1, elements=['feConvolveMatrix']), DefaultAttribute('diffuseConstant', 1, elements=['feDiffuseLighting']), DefaultAttribute('edgeMode', 'duplicate', elements=['feConvolveMatrix']), @@ -1848,21 +1851,16 @@ default_attributes = [ DefaultAttribute('offset', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), DefaultAttribute('operator', 'over', elements=['feComposite']), DefaultAttribute('operator', 'erode', elements=['feMorphology']), - # We pretend order is a string (because handling it as an - # SVGLength will cause issues when order is two integers). Note - # that order must be exactly one or two integers (no units or - # fancy numbers), so working with it a string will generally just - # work. 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('radius', '0', elements=['feMorphology']), DefaultAttribute('scale', 0, elements=['feDisplacementMap']), DefaultAttribute('seed', 0, elements=['feTurbulence']), DefaultAttribute('specularConstant', 1, elements=['feSpecularLighting']), DefaultAttribute('specularExponent', 1, elements=['feSpecularLighting', 'feSpotLight']), - # Pretend it is a string (for the same reasons as we do with "order") DefaultAttribute('stdDeviation', '0', elements=['feGaussianBlur']), DefaultAttribute('stitchTiles', 'noStitch', elements=['feTurbulence']), DefaultAttribute('surfaceScale', 1, elements=['feDiffuseLighting', 'feSpecularLighting']), From 06ea23d0e1715ae7eead5c2ff3cf4152e4d52b95 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 1 Jul 2018 13:52:51 +0200 Subject: [PATCH 215/270] fix typo --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index e2d87a5..8feb15c 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3849,7 +3849,7 @@ def parse_args(args=None, ignore_additional_args=False): if options.cdigits > options.digits: options.cdigits = -1 print("WARNING: The value for '--set-c-precision' should be lower than the value for '--set-precision'. " - "Number of significant digits for control points reset to defsault value, see --help", file=sys.stderr) + "Number of significant digits for control points reset to default value, see --help", file=sys.stderr) if options.indent_type not in ['tab', 'space', 'none']: _options_parser.error("Invalid value for --indent, see --help") if options.indent_depth < 0: From 7d28f5e051c34f262c7881b00286e6923b742e9a Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 1 Jul 2018 19:24:22 +0200 Subject: [PATCH 216/270] Improve handling of newlines Previously we added way to many and removed empty lines afterwards (potentially destructive if xml:space="preserve") Also adds proper indentation for comment nodes --- scour/scour.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 8feb15c..8ec0126 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3334,8 +3334,6 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): children = element.childNodes if children.length == 0: outParts.append('/>') - if indent_depth > 0: - outParts.append(newline) else: outParts.append('>') @@ -3361,16 +3359,15 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): outParts.extend(['<![CDATA[', child.nodeValue, ']]>']) # Comment node elif child.nodeType == Node.COMMENT_NODE: - outParts.extend(['<!--', child.nodeValue, '-->']) + outParts.extend([newline, indent_type * (indent_depth+1), '<!--', child.nodeValue, '-->']) # TODO: entities, processing instructions, what else? else: # ignore the rest pass if onNewLine: + outParts.append(newline) outParts.append(indent_type * indent_depth) outParts.extend(['</', element.nodeName, '>']) - if indent_depth > 0: - outParts.append(newline) return "".join(outParts) @@ -3632,13 +3629,6 @@ def scourString(in_string, options=None): # out_string = doc.documentElement.toprettyxml(' ') 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) - # return the string with its XML prolog and surrounding comments if options.strip_xml_prolog is False: total_output = '<?xml version="1.0" encoding="UTF-8"' @@ -3650,7 +3640,7 @@ def scourString(in_string, options=None): for child in doc.childNodes: if child.nodeType == Node.ELEMENT_NODE: - total_output += "".join(lines) + total_output += out_string else: # doctypes, entities, comments total_output += child.toxml() + '\n' From e1c2699f07ef205188cce679f34381cb81c59bbc Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Sun, 1 Jul 2018 20:16:51 +0200 Subject: [PATCH 217/270] Improve whitespace handling in text content elements SVG specifies special logic for handling whitespace, see https://www.w3.org/TR/SVG/text.html#WhiteSpace by implementing it we can even shave off some unneeded bytes here and there (e.g. consecutive spaces). Unfortunately handling of newlines by renderers is inconsistent: Sometimes they are replaced by a single space, sometimes they are removed in the output. As we can not know the expected behavior work around this by keeping newlines inside text content elements intact. Fixes #160. --- scour/scour.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 8ec0126..08027b8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3341,19 +3341,30 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): for child in element.childNodes: # element node if child.nodeType == Node.ELEMENT_NODE: - if preserveWhitespace: + # do not indent inside text content elements as in SVG there's a difference between + # "text1\ntext2" and + # "text1\n text2" + # see https://www.w3.org/TR/SVG/text.html#WhiteSpace + if preserveWhitespace or element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: outParts.append(serializeXML(child, options, 0, preserveWhitespace)) else: outParts.extend([newline, serializeXML(child, options, indent_depth + 1, preserveWhitespace)]) onNewLine = True # text node elif child.nodeType == Node.TEXT_NODE: - # 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())) + text_content = child.nodeValue + if not preserveWhitespace: + # strip / consolidate whitespace according to spec, see + # https://www.w3.org/TR/SVG/text.html#WhiteSpace + # As a workaround for inconsistent handling of renderers keep newlines if they were in the original + if element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: + text_content = text_content.replace('\t', ' ') + text_content = text_content.strip(' ') + while ' ' in text_content: + text_content = text_content.replace(' ', ' ') + else: + text_content = text_content.strip() + outParts.append(makeWellFormed(text_content)) # CDATA node elif child.nodeType == Node.CDATA_SECTION_NODE: outParts.extend(['<![CDATA[', child.nodeValue, ']]>']) From 2200f8dc819c4a330b49050569fb8aa0b01ff574 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Mon, 2 Jul 2018 01:05:54 +0200 Subject: [PATCH 218/270] temp --- scour/scour.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 08027b8..000142d 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3359,7 +3359,10 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): # As a workaround for inconsistent handling of renderers keep newlines if they were in the original if element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: text_content = text_content.replace('\t', ' ') - text_content = text_content.strip(' ') + if child == element.firstChild: + text_content = text_content.lstrip() + elif child == element.lastChild: + text_content = text_content.rstrip() while ' ' in text_content: text_content = text_content.replace(' ', ' ') else: From 703122369ed0c6a4e4217e7a77a8bf47d2ec3671 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Mon, 2 Jul 2018 22:14:14 +0200 Subject: [PATCH 219/270] Strip newlines from text nodes and be done with it Follow the spec "blindly" as it turns out covering all the border and getting reasonably styled output is just to cumbersome. This way at least scour output is consistent and it also saves us some bytes (a lot in some cases as we do not indent <tspan>s etc. anymore) --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 000142d..a46af61 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3356,8 +3356,8 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): if not preserveWhitespace: # strip / consolidate whitespace according to spec, see # https://www.w3.org/TR/SVG/text.html#WhiteSpace - # As a workaround for inconsistent handling of renderers keep newlines if they were in the original if element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: + text_content = text_content.replace('\n', '') text_content = text_content.replace('\t', ' ') if child == element.firstChild: text_content = text_content.lstrip() From 651694a6c0547a57f6634ad4331fa2d551c87074 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Tue, 3 Jul 2018 22:53:05 +0200 Subject: [PATCH 220/270] Add unittests for whitespace handling in text node Also expand/fix the test for line endings --- testscour.py | 103 +++++++++++++++++++++-------- unittests/newlines.svg | 50 ++++++++++++++ unittests/whitespace-important.svg | 4 -- unittests/whitespace-nested.svg | 4 -- unittests/whitespace.svg | 40 +++++++++++ 5 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 unittests/newlines.svg delete mode 100644 unittests/whitespace-important.svg delete mode 100644 unittests/whitespace-nested.svg create mode 100644 unittests/whitespace.svg diff --git a/testscour.py b/testscour.py index cc6676e..04da38c 100755 --- a/testscour.py +++ b/testscour.py @@ -1744,34 +1744,83 @@ class DoNotRemoveGradientsWhenReferencedInStyleCss(unittest.TestCase): 'Gradients removed when referenced in CSS') -class DoNotPrettyPrintWhenWhitespacePreserved(unittest.TestCase): +class Whitespace(unittest.TestCase): - def runTest(self): - with open('unittests/whitespace-important.svg') as f: - s = scourString(f.read()).splitlines() - c = '''<?xml version="1.0" encoding="UTF-8"?> -<svg xmlns="http://www.w3.org/2000/svg"> - <text xml:space="preserve">This is some <tspan font-style="italic">messed-up</tspan> markup</text> -</svg> -'''.splitlines() - for i in range(4): - self.assertEqual(s[i], c[i], - 'Whitespace not preserved for line ' + str(i)) + def setUp(self): + self.doc = scourXmlFile('unittests/whitespace.svg') + def test_basic(self): + text = self.doc.getElementById('txt_a1') + self.assertIn('text1 text2', text.toxml(), + 'Multiple spaces not stripped from text element') + text = self.doc.getElementById('txt_a2') + self.assertIn('text1 text2', text.toxml(), + 'Tab not replaced with space in text element') + text = self.doc.getElementById('txt_a3') + self.assertIn('text1 text2', text.toxml(), + 'Multiple spaces not stripped from text element with xml:space="default"') + text = self.doc.getElementById('txt_a4') + self.assertIn('text1 text2', text.toxml(), + 'Tab not replaced with space in text element with xml:space="default"') + text = self.doc.getElementById('txt_a5') + self.assertIn('text1 text2', text.toxml(), + 'Multiple spaces not preserved in text element with xml:space="preserve"') + text = self.doc.getElementById('txt_a6') + self.assertIn('text1\ttext2', text.toxml(), + 'Tab not preserved in text element with xml:space="preserve"') -class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): + def test_newlines(self): + text = self.doc.getElementById('txt_b1') + self.assertIn('text1 text2', text.toxml(), + 'Newline not replaced with space in text element') + text = self.doc.getElementById('txt_b2') + self.assertIn('text1 text2', text.toxml(), + 'Newline not replaced with space in text element with xml:space="default"') + text = self.doc.getElementById('txt_b3') + self.assertIn('text1\n text2', text.toxml(), + 'Newline not preserved in text element with xml:space="preserve"') - def runTest(self): - with open('unittests/whitespace-nested.svg') as f: - s = scourString(f.read()).splitlines() - c = '''<?xml version="1.0" encoding="UTF-8"?> -<svg xmlns="http://www.w3.org/2000/svg"> - <text xml:space="preserve"><tspan font-style="italic">Use <tspan font-style="bold">bold</tspan> text</tspan></text> -</svg> -'''.splitlines() - for i in range(4): - self.assertEqual(s[i], c[i], - 'Whitespace not preserved when nested for line ' + str(i)) + def test_inheritance(self): + text = self.doc.getElementById('txt_c1') + self.assertIn('text1 text2', text.toxml(), + '<tspan> does not inherit xml:space="preserve" of parent text element') + text = self.doc.getElementById('txt_c2') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="default" of <tspan> does not overwrite xml:space="preserve" of parent text element') + text = self.doc.getElementById('txt_c3') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="preserve" of <tspan> does not overwrite xml:space="default" of parent text element') + text = self.doc.getElementById('txt_c4') + self.assertIn('text1 text2', text.toxml(), + '<text> does not inherit xml:space="preserve" of parent group') + text = self.doc.getElementById('txt_c5') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="default" of text element does not overwrite xml:space="preserve" of parent group') + text = self.doc.getElementById('txt_c6') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="preserve" of text element does not overwrite xml:space="default" of parent group') + + def test_important_whitespace(self): + text = self.doc.getElementById('txt_d1') + self.assertIn('text1 text2', text.toxml(), + 'Newline with whitespace collapsed in text element') + text = self.doc.getElementById('txt_d2') + self.assertIn('text1 <tspan>tspan1</tspan> text2', text.toxml(), + 'Whitespace stripped from the middle of a text element') + text = self.doc.getElementById('txt_d3') + self.assertIn('text1 <tspan>tspan1 <tspan>tspan2</tspan> text2</tspan>', text.toxml(), + 'Whitespace stripped from the middle of a text element') + + def test_incorrect_whitespace(self): + text = self.doc.getElementById('txt_e1') + self.assertIn('text1text2', text.toxml(), + 'Whitespace introduced in text element with newline') + text = self.doc.getElementById('txt_e2') + self.assertIn('text1<tspan>tspan</tspan>text2', text.toxml(), + 'Whitespace introduced in text element with <tspan>') + text = self.doc.getElementById('txt_e3') + self.assertIn('text1<tspan>tspan</tspan>text2', text.toxml(), + 'Whitespace introduced in text element with <tspan> and newlines') class GetAttrPrefixRight(unittest.TestCase): @@ -1807,10 +1856,10 @@ class HandleEmptyStyleElement(unittest.TestCase): class EnsureLineEndings(unittest.TestCase): def runTest(self): - with open('unittests/whitespace-important.svg') as f: + with open('unittests/newlines.svg') as f: s = scourString(f.read()) - self.assertEqual(len(s.splitlines()), 4, - 'Did not output line ending character correctly') + self.assertEqual(len(s.splitlines()), 24, + 'Did handle reading or outputting line ending characters correctly') class XmlEntities(unittest.TestCase): diff --git a/unittests/newlines.svg b/unittests/newlines.svg new file mode 100644 index 0000000..a909603 --- /dev/null +++ b/unittests/newlines.svg @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> + + + +<svg xmlns="http://www.w3.org/2000/svg" + +> + + + + <!-- this file has pretty messed up formatting --> <rect width="100" height="100"/> + <rect width="100" height="100"/> + <rect width="100" height="100"/> + + + + + + <rect width="100" height="100"/> + <rect width="100" height="100"/> + <rect width="100" height="100"/> + + + + + + + <!-- we have mixed newline + characters, carriage returns and both of them + as well as tabs and spaces + --> + + <rect width="100" height="100"/><rect width="100" height="100"/> <rect width="100" height="100"/> + + <rect width="100" height="100"/> <rect width="100" height="100"/> <rect width="100" height="100"/> + + + + + + <rect width="100" height="100"/> <rect width="100" height="100"/> + + +</svg> + + + + + +<!-- OMG, really? --> \ No newline at end of file diff --git a/unittests/whitespace-important.svg b/unittests/whitespace-important.svg deleted file mode 100644 index 6918044..0000000 --- a/unittests/whitespace-important.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg"> - <text xml:space="preserve">This is some <tspan font-style="italic">messed-up</tspan> markup</text> -</svg> diff --git a/unittests/whitespace-nested.svg b/unittests/whitespace-nested.svg deleted file mode 100644 index 3b99356..0000000 --- a/unittests/whitespace-nested.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg"> - <text xml:space="preserve"><tspan font-style="italic">Use <tspan font-style="bold">bold</tspan> text</tspan></text> -</svg> diff --git a/unittests/whitespace.svg b/unittests/whitespace.svg new file mode 100644 index 0000000..2bb48a6 --- /dev/null +++ b/unittests/whitespace.svg @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <!-- basic tests --> + <text id="txt_a1">text1 text2</text> <!-- multiple spaces --> + <text id="txt_a2">text1 text2</text> <!-- tab --> + <text id="txt_a3" xml:space="default">text1 text2</text> <!-- multiple spaces --> + <text id="txt_a4" xml:space="default">text1 text2</text> <!-- tab --> + <text id="txt_a5" xml:space="preserve">text1 text2</text> <!-- multiple spaces --> + <text id="txt_a6" xml:space="preserve">text1 text2</text> <!-- tab --> + + <!-- newlines --> + <text id="txt_b1">text1 + text2</text> + <text id="txt_b2" xml:space="default">text1 + text2</text> + <text id="txt_b3" xml:space="preserve">text1 + text2</text> + + <!-- inheritance --> + <text id="txt_c1" xml:space="preserve"><tspan>text1 text2</tspan></text> + <text id="txt_c2" xml:space="preserve"><tspan xml:space="default">text1 text2</tspan></text> + <text id="txt_c3" xml:space="default"><tspan xml:space="preserve">text1 text2</tspan></text> + <g xml:space="preserve"><text id="txt_c4">text1 text2</text></g> + <g xml:space="preserve"><text id="txt_c5" xml:space="default">text1 text2</text></g> + <g xml:space="default"><text id="txt_c6" xml:space="preserve">text1 text2</text></g> + + <!-- important whitespace that must not be stripped --> + <text id="txt_d1">text1 + text2</text> + <text id="txt_d2">text1 <tspan>tspan1</tspan> text2</text> + <text id="txt_d3">text1 <tspan>tspan1 <tspan>tspan2</tspan> text2</tspan></text> + + <!-- whitespace must not be introduced --> + <text id="txt_e1">text1 +text2</text> + <text id="txt_e2">text1<tspan>tspan</tspan>text2</text> + <text id="txt_e3">text1 +<tspan>tspan</tspan> +text2</text> +</svg> From 5ccba31ff944c755f7c6d4cd523324be46b5ab67 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Wed, 4 Jul 2018 19:05:25 +0200 Subject: [PATCH 221/270] Update HISTORY.md --- HISTORY.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index ed88f93..3ba52d2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,7 +1,19 @@ # Release Notes for Scour +## Version 0.37 (2018-07-04) +* Fix escaping of quotes in attribute values. ([#152](https://github.com/scour-project/scour/pull/152)) +* A lot of performance improvements making processing significantly faster in many cases. ([#167](https://github.com/scour-project/scour/pull/167), [#169](https://github.com/scour-project/scour/pull/169), [#171](https://github.com/scour-project/scour/pull/171), [#185](https://github.com/scour-project/scour/pull/185)) +* Fix exception when removing duplicated gradients while `--keep-unreferenced-defs` is used ([#173](https://github.com/scour-project/scour/pull/173)) +* Remove some illegal optimizations of `m0 0` sub-path commands ([#178](https://github.com/scour-project/scour/pull/178)) +* Fix and improve handling of boolean flags in elliptical arc path commands ([#183](https://github.com/scour-project/scour/pull/183)) +* Fix exception when shorthand transform `scale(1)` with single number is used ([#191](https://github.com/scour-project/scour/pull/191)) +* Fix exception when using two-number forms of the filter attributes `baseFrequency`, `order`, `radius` and `stdDeviation` ([#192](https://github.com/scour-project/scour/pull/192)) +* Improve whitespace handling in text nodes fixing an issue where scouring added spaces in error and reducing file size in many cases ([#199](https://github.com/scour-project/scour/pull/199)) +* Drop official support for Python 3.3. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 3.3 and should find/fix any compatibility issues pull requests are welcome, though.) + + ## Version 0.36 (2017-08-06) -* Fix embedding of raster images which was broken in most cases and did not work at all in Python 3. ([#120](https://github.com/scour-project/scour/issues/62)) +* Fix embedding of raster images which was broken in most cases and did not work at all in Python 3. ([#120](https://github.com/scour-project/scour/issues/120)) * Some minor fixes for statistics output. * Greatly improve the algorithm to reduce numeric precision. * Precision was not properly reduced for some numbers. @@ -14,7 +26,7 @@ * Attempt to collapse lineto `l` commands into a preceding moveto `m` command (these are then called "implicit lineto commands") * Do not collapse straight path segments in paths that have intermediate markers. ([#145](https://github.com/scour-project/scour/issues/145)) * Preserve empty path segments if they have `stroke-linecap` set to `round` or `square`. They render no visible line but a tiny dot or square. - + ## Version 0.35 (2016-09-14) From 049264eba6b1a54ae5ba1d6a5077d8e7b80e8835 Mon Sep 17 00:00:00 2001 From: Eduard Braun <eduard.braun2@gmx.de> Date: Wed, 4 Jul 2018 19:16:55 +0200 Subject: [PATCH 222/270] Scour v0.37 --- scour/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/__init__.py b/scour/__init__.py index 5831769..f3d44ee 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ # ############################################################################### -__version__ = u'0.36' +__version__ = u'0.37' From 695676e3a52d6d4dba29b43cd8d872fd29fdbd16 Mon Sep 17 00:00:00 2001 From: Patrick Storz <eduard.braun2@gmx.de> Date: Sun, 17 May 2020 15:52:04 +0200 Subject: [PATCH 223/270] Run tests with Python 3.7 / 3.8 --- .travis.yml | 6 ++++-- tox.ini | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f937d8..991bb30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ python: - 3.4 - 3.5 - 3.6 + - 3.7 + - 3.8 install: - pip install tox-travis codecov @@ -18,9 +20,9 @@ matrix: fast_finish: true include: - - python: 3.6 + - python: 3.8 env: - TOXENV=flake8 after_success: - - coverage combine && codecov \ No newline at end of file + - coverage combine && codecov diff --git a/tox.ini b/tox.ini index 5c67f5b..b1b36b0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ envlist = py34 py35 py36 + py37 + py38 flake8 @@ -24,4 +26,4 @@ deps = flake8 commands = - flake8 --max-line-length=119 \ No newline at end of file + flake8 --max-line-length=119 From 09a656287de73b61db2c5e5895d13e4482e48722 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 18 Aug 2018 16:18:58 +0000 Subject: [PATCH 224/270] Avoid picking an id-less gradient to replace one with an id Closes: #203 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index a46af61..61048e8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1395,6 +1395,19 @@ def removeDuplicateGradients(doc): continue master = bucket[0] duplicates = bucket[1:] + master_id = master.getAttribute('id') + if not master_id: + # If our selected "master" copy does not have an ID, + # then replace it with one that does (assuming any of + # them has one). This avoids broken images like we + # saw in GH#203 + for i in range(len(duplicates)): + dup = duplicates[i] + dup_id = dup.getAttribute('id') + if dup_id: + duplicates[i] = master + master = dup + break gradientsToRemove[master] = duplicates From f61b4d36d6db0a810054586a8d891ecc31d7a02e Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 18 Aug 2018 16:27:33 +0000 Subject: [PATCH 225/270] Add test case for #203 Signed-off-by: Niels Thykier <niels@thykier.net> --- testscour.py | 14 ++++++++++++++ ...ve-duplicate-gradients-master-without-id.svg | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 unittests/remove-duplicate-gradients-master-without-id.svg diff --git a/testscour.py b/testscour.py index 04da38c..b2f24e6 100755 --- a/testscour.py +++ b/testscour.py @@ -1401,6 +1401,20 @@ class RemoveDuplicateRadialGradients(unittest.TestCase): 'Duplicate radial gradient not removed') +class RemoveDuplicateRadialGradientsEnsureMasterHasID(unittest.TestCase): + + def runTest(self): + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients-master-without-id.svg') + lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') + rect = svgdoc.getElementById('r1') + self.assertEqual(1, lingrads.length, + 'Duplicate linearGradient not removed') + self.assertEqual(lingrads[0].getAttribute("id"), "g1", + "linearGradient has a proper ID") + self.assertNotEqual(rect.getAttribute("fill"), "url(#)", + "linearGradient has a proper ID") + + class RereferenceForRadialGradient(unittest.TestCase): def runTest(self): diff --git a/unittests/remove-duplicate-gradients-master-without-id.svg b/unittests/remove-duplicate-gradients-master-without-id.svg new file mode 100644 index 0000000..66727e9 --- /dev/null +++ b/unittests/remove-duplicate-gradients-master-without-id.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <linearGradient x1="0" y1="0" x2="1" y2="1"> + <stop offset="0" stop-color="red"/> + <stop offset="1" stop-color="blue"/> + </linearGradient> + <linearGradient id='g1' x1='0' y1='0' x2='1' y2='1'> + <stop offset='0' stop-color='red'/> + <stop offset='1' stop-color='blue'/> + </linearGradient> + <linearGradient x1='0' y1='0' x2='1' y2='1'> + <stop offset='0' stop-color='red'/> + <stop offset='1' stop-color='blue'/> + </linearGradient> + + <rect id="r1" fill="url(#g1)" width="100" height="100"/> +</svg> From 6846e0c9ee5cb985a886e07d0ab9b899172d91aa Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 18 Aug 2018 15:18:18 +0000 Subject: [PATCH 226/270] Preserve xhref:href attr when collapsing referenced gradients Closes: #198 Closes: #202 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 61048e8..c8bcaf8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1338,8 +1338,16 @@ def collapseSinglyReferencedGradients(doc): 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') + target_href = elem.getAttributeNS(NS['XLINK'], 'href') + if target_href: + # If the elem node had an xlink:href, then the + # refElem have to point to it as well to + # perserve the semantics of the image. + refElem.setAttributeNS(NS['XLINK'], 'href', target_href) + else: + # The elem node had no xlink:href reference, + # so we can simply remove the attribute. + refElem.removeAttributeNS(NS['XLINK'], 'href') # now delete elem elem.parentNode.removeChild(elem) From 58b75c314ae56da3fbe5d2b46ff0b1ba66c5a0c9 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sat, 18 Aug 2018 15:48:23 +0000 Subject: [PATCH 227/270] Add test case for #198/#202 Signed-off-by: Niels Thykier <niels@thykier.net> --- testscour.py | 14 ++++++++++++++ .../collapse-gradients-preserve-xlink-href.svg | 13 +++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 unittests/collapse-gradients-preserve-xlink-href.svg diff --git a/testscour.py b/testscour.py index b2f24e6..653e278 100755 --- a/testscour.py +++ b/testscour.py @@ -891,6 +891,20 @@ class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase): 'Multiply-referenced linear gradient collapsed') +class PreserveXLinkHrefWhenCollapsingReferencedGradients(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/collapse-gradients-preserve-xlink-href.svg') + g1 = doc.getElementById("g1") + g2 = doc.getElementById("g2") + g3 = doc.getElementById("g3") + self.assertTrue(g1, 'g1 is still present') + self.assertTrue(g2 is None, 'g2 was removed') + self.assertTrue(g3, 'g3 is still present') + self.assertEqual(g3.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#g1', + 'g3 has a xlink:href to g1') + + class RemoveTrailingZerosFromPath(unittest.TestCase): def runTest(self): diff --git a/unittests/collapse-gradients-preserve-xlink-href.svg b/unittests/collapse-gradients-preserve-xlink-href.svg new file mode 100644 index 0000000..f736922 --- /dev/null +++ b/unittests/collapse-gradients-preserve-xlink-href.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<defs> + <linearGradient id="g1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="blue" /> + <stop offset="1" stop-color="yellow" /> + </linearGradient> + <radialGradient id="g2" xlink:href="#g1" cx="100" cy="100" r="70"/> + <radialGradient id="g3" xlink:href="#g2" cx="100" cy="100" r="70"/> +</defs> +<rect fill="url(#g1)" width="200" height="200"/> +<rect fill="url(#g3)" width="200" height="200" y="200"/> +</svg> From f65ca60809d7d8e380a1bcae90def73ac3560f93 Mon Sep 17 00:00:00 2001 From: Patrick Storz <eduard.braun2@gmx.de> Date: Sun, 17 May 2020 16:51:56 +0200 Subject: [PATCH 228/270] Fix deprecation warning --- testscour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testscour.py b/testscour.py index 708160b..68521ce 100755 --- a/testscour.py +++ b/testscour.py @@ -2054,8 +2054,8 @@ class ShortenIDsStableOutput(unittest.TestCase): hrefs_ordered = [x.getAttributeNS('http://www.w3.org/1999/xlink', 'href') for x in use_tags] expected = ['#a', '#b', '#b'] - self.assertEquals(hrefs_ordered, expected, - '--shorten-ids pointlessly reassigned ids') + self.assertEqual(hrefs_ordered, expected, + '--shorten-ids pointlessly reassigned ids') class MustKeepGInSwitch(unittest.TestCase): From 40753af88a1570307a1ce08fb1f5a967c8545793 Mon Sep 17 00:00:00 2001 From: Patrick Storz <eduard.braun2@gmx.de> Date: Sun, 17 May 2020 17:33:50 +0200 Subject: [PATCH 229/270] Fix whitespace handling for SVG 1.2 flowed text See 718748ff22ca4477a884035bfbf2175639043275 Fixes https://github.com/scour-project/scour/issues/235 --- scour/scour.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 64bf9bf..661dd9a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3354,6 +3354,10 @@ def chooseQuoteCharacter(str): return (quote, hasEmbeddedQuote) +TEXT_CONTENT_ELEMENTS = ['text', 'tspan', 'tref', 'textPath', 'altGlyph', + 'flowDiv', 'flowPara', 'flowSpan', 'flowTref', 'flowLine'] + + # hand-rolled serialization function that has the following benefits: # - pretty printing # - somewhat judicious use of whitespace @@ -3437,7 +3441,7 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): # "text1\ntext2" and # "text1\n text2" # see https://www.w3.org/TR/SVG/text.html#WhiteSpace - if preserveWhitespace or element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: + if preserveWhitespace or element.nodeName in TEXT_CONTENT_ELEMENTS: outParts.append(serializeXML(child, options, 0, preserveWhitespace)) else: outParts.extend([newline, serializeXML(child, options, indent_depth + 1, preserveWhitespace)]) @@ -3448,7 +3452,7 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): if not preserveWhitespace: # strip / consolidate whitespace according to spec, see # https://www.w3.org/TR/SVG/text.html#WhiteSpace - if element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: + if element.nodeName in TEXT_CONTENT_ELEMENTS: text_content = text_content.replace('\n', '') text_content = text_content.replace('\t', ' ') if child == element.firstChild: From dd2155e5764d13e03ee3702fdf78d48628a84988 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 20 Mar 2018 21:34:20 +0000 Subject: [PATCH 230/270] Merge sibling <g> nodes with identical attributes In some cases, gnuplot generates a very suboptimal SVG content of the following pattern: <g color="black" fill="none" stroke="currentColor"> <path d="m82.5 323.3v-4.1" stroke="#000"/> </g> <g color="black" fill="none" stroke="currentColor"> <path d="m116.4 323.3v-4.1" stroke="#000"/> </g> ... repeated 10+ more times here ... <g color="black" fill="none" stroke="currentColor"> <path d="m65.4 72.8v250.5h420v-250.5h-420z" stroke="#000"/> </g> A more optimal pattern would be: <g color="black" fill="none" stroke="#000"> <path d="m82.5 323.3v-4.1"/> <path d="m116.4 323.3v-4.1"/> ... 10+ more paths here ... <path d="m65.4 72.8v250.5h420v-250.5h-420z"/> </g> This patch enables that optimization by handling the merging of two sibling <g> entries that have identical attributes. In the above example that does not solve the rewrite from "currentColor" to "#000" for the stroke attribute. However, the existing code already handles that automatically after the <g> elements have been merged. This change provides comparable results to --create-groups as shown by the following diagram while being a distinct optimization: +----------------------------+-------+--------+ | Test | Size | in % | +----------------------------+-------+--------+ | baseline | 17961 | 100% | | baseline + --create-groups | 17418 | 97.0% | | patched | 16939 | 94.3% | | patched + --create-groups | 16855 | 93.8% | +----------------------------+-------+--------+ The image used in the size table above was generated based on the instructions from https://bugs.debian.org/858039#10 with gnuplot 5.2 patchlevel 2. Beyond the test-based "--create-groups", the following scour command-line parameters were used: --enable-id-stripping --enable-comment-stripping \ --shorten-ids --indent=none Note that the baseline was scour'ed repeatedly to stablize the image size. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 71 +++++++++++++++++++++++++++++++ testscour.py | 15 +++++++ unittests/group-sibling-merge.svg | 29 +++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 unittests/group-sibling-merge.svg diff --git a/scour/scour.py b/scour/scour.py index 661dd9a..4b71ad1 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1134,6 +1134,75 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): return num +def mergeSiblingGroupsWithCommonAttributes(elem): + """ + Merge two or more sibling <g> elements with the identical attributes. + + This function acts recursively on the given element. + """ + + num = 0 + i = elem.childNodes.length - 1 + while i >= 0: + currentNode = elem.childNodes.item(i) + if currentNode.nodeType != Node.ELEMENT_NODE or currentNode.nodeName != 'g' or \ + currentNode.namespaceURI != NS['SVG']: + i -= 1 + continue + attributes = {a.nodeName: a.nodeValue for a in currentNode.attributes.values()} + runStart, runEnd = i, i + runElements = 1 + while runStart > 0: + nextNode = elem.childNodes.item(runStart - 1) + if nextNode.nodeType == Node.ELEMENT_NODE: + if nextNode.nodeName != 'g' or nextNode.namespaceURI != NS['SVG']: + break + nextAttributes = {a.nodeName: a.nodeValue for a in nextNode.attributes.values()} + hasNoMergeTags = (True for n in nextNode.childNodes + if n.nodeType == Node.ELEMENT_NODE + and n.nodeName in ('title', 'desc') + and n.namespaceURI == NS['SVG']) + if attributes != nextAttributes or any(hasNoMergeTags): + break + else: + runElements += 1 + runStart -= 1 + else: + runStart -= 1 + + # Next loop will start from here + i = runStart - 1 + + if runElements < 2: + continue + + # Find the <g> entry that starts the run (we might have run + # past it into a text node or a comment node. + while True: + node = elem.childNodes.item(runStart) + if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: + break + runStart += 1 + primaryGroup = elem.childNodes.item(runStart) + runStart += 1 + nodes = elem.childNodes[runStart:runEnd+1] + for node in nodes: + if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: + # Merge + primaryGroup.childNodes.extend(node.childNodes) + node.childNodes = [] + else: + primaryGroup.childNodes.append(node) + elem.childNodes.remove(node) + + # each child gets the same treatment, recursively + for childNode in elem.childNodes: + if childNode.nodeType == Node.ELEMENT_NODE: + num += mergeSiblingGroupsWithCommonAttributes(childNode) + + return num + + def createGroupsForCommonAttributes(elem): """ Creates <g> elements to contain runs of 3 or more @@ -3658,6 +3727,8 @@ def scourString(in_string, options=None): while removeDuplicateGradients(doc) > 0: pass + if options.group_collapse: + _num_elements_removed += mergeSiblingGroupsWithCommonAttributes(doc.documentElement) # create <g> elements if there are runs of elements with the same attributes. # this MUST be before moveCommonAttributesToParentGroup. if options.group_create: diff --git a/testscour.py b/testscour.py index 68521ce..d9a460b 100755 --- a/testscour.py +++ b/testscour.py @@ -2075,6 +2075,21 @@ class MustKeepGInSwitch2(unittest.TestCase): 'Erroneously removed a <g> in a <switch>') +class GroupSiblingMerge(unittest.TestCase): + + def test_sibling_merge(self): + doc = scourXmlFile('unittests/group-sibling-merge.svg', + parse_args([])) + self.assertEqual(doc.getElementsByTagName('g').length, 5, + 'Merged sibling <g> tags with similar values') + + def test_sibling_merge_disabled(self): + doc = scourXmlFile('unittests/group-sibling-merge.svg', + parse_args(['--disable-group-collapsing'])) + self.assertEqual(doc.getElementsByTagName('g').length, 8, + 'Sibling merging is disabled by --disable-group-collapsing') + + class GroupCreation(unittest.TestCase): def runTest(self): diff --git a/unittests/group-sibling-merge.svg b/unittests/group-sibling-merge.svg new file mode 100644 index 0000000..0a2181c --- /dev/null +++ b/unittests/group-sibling-merge.svg @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg"> +<desc>Produced by GNUPLOT 5.2 patchlevel 8</desc> +<rect width="900" height="600" fill="none"/> +<g color="black" fill="none"> +<path d="m88.5 564h9m777.5 0h-9" stroke="#000"/> +<g transform="translate(80.2,567.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> +<text><tspan font-family="Arial">0</tspan></text> +</g> +</g> +<g color="black" fill="none"> +<path d="m88.5 473h9m777.5 0h-9" stroke="#000"/> +<g transform="translate(80.2,476.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> +<text><tspan font-family="Arial">5000</tspan></text> +</g> +</g> +<g color="black" fill="none"> +<path d="m88.5 382h9m777.5 0h-9" stroke="#000"/> +<g transform="translate(80.2,385.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> +<text><tspan font-family="Arial">10000</tspan></text> +</g> +</g> +<g color="black" fill="none"> +<path d="m88.5 291h9m777.5 0h-9" stroke="#000"/> +<g transform="translate(80.2,294.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> +<text><tspan font-family="Arial">15000</tspan></text> +</g> +</g> +</svg> From a15acb3e4e254d58d01d61f97bb96f41a925ec4f Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 17 May 2020 17:55:24 +0000 Subject: [PATCH 231/270] Rename testX.py to test_X.py to make py.test work out of the box (#181) This rename makes py.test/py.test-3 find the test suite out of the box. Example command lines: # Running the test suite (optionally include "-v") $ py.test-3 # Running the test suite with coverage enabled (and branch # coverage). $ py.test-3 --cov=scour --cov-report=html --cov-branch Signed-off-by: Niels Thykier <niels@thykier.net> --- CONTRIBUTING.md | 2 +- Makefile | 6 +++--- testcss.py => test_css.py | 0 testscour.py => test_scour.py | 2 +- tox.ini | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename testcss.py => test_css.py (100%) rename testscour.py => test_scour.py (99%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b239cc..b34dbf1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ In order to check functionality of Scour and prevent any regressions in existing make test ``` -These tests are run automatically on all PRs using [TravisCI](https://travis-ci.org/scour-project/scour) and have to pass at all times! When you add new functionality you should always include suitable tests with your PR (see [`testscour.py`](https://github.com/scour-project/scour/blob/master/testscour.py)). +These tests are run automatically on all PRs using [TravisCI](https://travis-ci.org/scour-project/scour) and have to pass at all times! When you add new functionality you should always include suitable tests with your PR (see [`test_scour.py`](https://github.com/scour-project/scour/blob/master/test_scour.py)). ### Coverage diff --git a/Makefile b/Makefile index 52323c1..09389b5 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ check: test flake8 test: - python testscour.py + python test_scour.py test_version: PYTHONPATH=. python -m scour.scour --version @@ -34,6 +34,6 @@ flake8: flake8 --max-line-length=119 coverage: - coverage run --source=scour testscour.py + coverage run --source=scour test_scour.py coverage html - coverage report \ No newline at end of file + coverage report diff --git a/testcss.py b/test_css.py similarity index 100% rename from testcss.py rename to test_css.py diff --git a/testscour.py b/test_scour.py similarity index 99% rename from testscour.py rename to test_scour.py index d9a460b..6c4c7ce 100755 --- a/testscour.py +++ b/test_scour.py @@ -2774,7 +2774,7 @@ class ViewBox(unittest.TestCase): # TODO: write tests for --keep-editor-data if __name__ == '__main__': - testcss = __import__('testcss') + testcss = __import__('test_css') scour = __import__('__main__') suite = unittest.TestSuite(list(map(unittest.defaultTestLoader.loadTestsFromModule, [testcss, scour]))) unittest.main(defaultTest="suite") diff --git a/tox.ini b/tox.ini index b1b36b0..238864d 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ deps = commands = scour --version - coverage run --parallel-mode --source=scour testscour.py + coverage run --parallel-mode --source=scour test_scour.py [testenv:flake8] From 47e8b15315b6a3f76bf8432de1024c230e75fa58 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Sun, 7 Jun 2020 16:35:46 +0000 Subject: [PATCH 232/270] convertColors: Fix bug in computation in how many bytes are saved (#245) Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 4b71ad1..638ee5f 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2205,7 +2205,7 @@ def convertColors(element): newBytes = len(newColorValue) if oldBytes > newBytes: styles[attr] = newColorValue - numBytes += (oldBytes - len(element.getAttribute(attr))) + numBytes += (oldBytes - newBytes) _setStyle(element, styles) # now recurse for our child elements From 21f1262bcb68e3395ce1cff9fb9284291b3dd066 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 19 May 2020 18:18:29 +0000 Subject: [PATCH 233/270] Avoid creating single-use-throw-away lists for string join There is no need to create a list of it only to discard it after a single use with join (which gladly accepts an iterator/generator instead). Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 638ee5f..150b8e4 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -562,7 +562,7 @@ def findReferencedElements(node, ids=None): # 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]) + stylesheet = "".join(child.nodeValue for child in node.childNodes) if stylesheet != '': cssRules = parseCssString(stylesheet) for rule in cssRules: @@ -853,7 +853,7 @@ def renameID(idFrom, idTo, identifiedElements, referringNodes): # 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]) + 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 + ')') @@ -1617,7 +1617,7 @@ def _getStyle(node): def _setStyle(node, styleMap): u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" - fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap]) + fixedStyle = ';'.join(prop + ':' + styleMap[prop] for prop in styleMap) if fixedStyle != '': node.setAttribute('style', fixedStyle) elif node.getAttribute('style'): @@ -2837,18 +2837,18 @@ def serializePath(pathObj, options): """ # 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, - control_points=controlPoints(cmd, data), - flags=flags(cmd, data)) - for cmd, data in pathObj]) + return ''.join(cmd + scourCoordinates(data, options, + control_points=controlPoints(cmd, data), + flags=flags(cmd, data)) + 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]) + return ' '.join(command + '(' + ' '.join(scourUnitlessLength(number) for number in numbers) + ')' + for command, numbers in transformObj) def scourCoordinates(data, options, force_whitespace=False, control_points=[], flags=[]): @@ -3408,7 +3408,7 @@ def makeWellFormed(str, quote=''): xml_ents = {'<': '<', '>': '>', '&': '&'} if quote: xml_ents[quote] = ''' if (quote == "'") else """ - return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) + return ''.join(xml_ents[c] if c in xml_ents else c for c in str) def chooseQuoteCharacter(str): @@ -3477,7 +3477,7 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): if attr.nodeName == 'style': # sort declarations - attrValue = ';'.join([p for p in sorted(attrValue.split(';'))]) + attrValue = ';'.join(sorted(attrValue.split(';'))) outParts.append(' ') # preserve xmlns: if it is a namespace prefix declaration From 5be6b03d7cb48c4354e5f7f313b98292cb88d72d Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 19 May 2020 21:36:58 +0000 Subject: [PATCH 234/270] Serialization: Avoid creating a single-use dict in each call to make_well_formed Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 40 +++++++++++++++++++++++----------------- test_scour.py | 21 +++++++++++---------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 150b8e4..15de2bd 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -74,6 +74,12 @@ VER = __version__ COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' +XML_ENTS_NO_QUOTES = {'<': '<', '>': '>', '&': '&'} +XML_ENTS_ESCAPE_APOS = XML_ENTS_NO_QUOTES.copy() +XML_ENTS_ESCAPE_APOS["'"] = ''' +XML_ENTS_ESCAPE_QUOT = XML_ENTS_NO_QUOTES.copy() +XML_ENTS_ESCAPE_QUOT['"'] = '"' + NS = {'SVG': 'http://www.w3.org/2000/svg', 'XLINK': 'http://www.w3.org/1999/xlink', 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', @@ -3404,23 +3410,23 @@ def remapNamespacePrefix(node, oldprefix, newprefix): remapNamespacePrefix(child, oldprefix, newprefix) -def makeWellFormed(str, quote=''): - xml_ents = {'<': '<', '>': '>', '&': '&'} - if quote: - xml_ents[quote] = ''' if (quote == "'") else """ - return ''.join(xml_ents[c] if c in xml_ents else c for c in str) +def make_well_formed(text, quote_dict=None): + if quote_dict is None: + quote_dict = XML_ENTS_NO_QUOTES + return ''.join(quote_dict[c] if c in quote_dict else c for c in text) -def chooseQuoteCharacter(str): - quotCount = str.count('"') - aposCount = str.count("'") - if quotCount > aposCount: - quote = "'" - hasEmbeddedQuote = aposCount - else: +def choose_quote_character(value): + quot_count = value.count('"') + if quot_count == 0 or quot_count <= value.count("'"): + # Fewest "-symbols (if there are 0, we pick this to avoid spending + # time counting the '-symbols as it won't matter) quote = '"' - hasEmbeddedQuote = quotCount - return (quote, hasEmbeddedQuote) + xml_ent = XML_ENTS_ESCAPE_QUOT + else: + quote = "'" + xml_ent = XML_ENTS_ESCAPE_APOS + return quote, xml_ent TEXT_CONTENT_ELEMENTS = ['text', 'tspan', 'tref', 'textPath', 'altGlyph', @@ -3472,8 +3478,8 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): attr = attrList.item(index) attrValue = attr.nodeValue - (quote, hasEmbeddedQuote) = chooseQuoteCharacter(attrValue) - attrValue = makeWellFormed(attrValue, quote if hasEmbeddedQuote else '') + quote, xml_ent = choose_quote_character(attrValue) + attrValue = make_well_formed(attrValue, xml_ent) if attr.nodeName == 'style': # sort declarations @@ -3532,7 +3538,7 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): text_content = text_content.replace(' ', ' ') else: text_content = text_content.strip() - outParts.append(makeWellFormed(text_content)) + outParts.append(make_well_formed(text_content)) # CDATA node elif child.nodeType == Node.CDATA_SECTION_NODE: outParts.extend(['<![CDATA[', child.nodeValue, ']]>']) diff --git a/test_scour.py b/test_scour.py index 6c4c7ce..e55b9db 100755 --- a/test_scour.py +++ b/test_scour.py @@ -30,7 +30,8 @@ import unittest import six from six.moves import map, range -from scour.scour import makeWellFormed, parse_args, scourString, scourXmlFile, start, run +from scour.scour import (make_well_formed, parse_args, scourString, scourXmlFile, start, run, + XML_ENTS_ESCAPE_APOS, XML_ENTS_ESCAPE_QUOT) from scour.svg_regex import svg_parser from scour import __version__ @@ -1893,26 +1894,26 @@ class EnsureLineEndings(unittest.TestCase): class XmlEntities(unittest.TestCase): def runTest(self): - self.assertEqual(makeWellFormed('<>&'), '<>&', + self.assertEqual(make_well_formed('<>&'), '<>&', 'Incorrectly translated unquoted XML entities') - self.assertEqual(makeWellFormed('<>&', "'"), '<>&', + self.assertEqual(make_well_formed('<>&', XML_ENTS_ESCAPE_APOS), '<>&', 'Incorrectly translated single-quoted XML entities') - self.assertEqual(makeWellFormed('<>&', '"'), '<>&', + self.assertEqual(make_well_formed('<>&', XML_ENTS_ESCAPE_QUOT), '<>&', 'Incorrectly translated double-quoted XML entities') - self.assertEqual(makeWellFormed("'"), "'", + self.assertEqual(make_well_formed("'"), "'", 'Incorrectly translated unquoted single quote') - self.assertEqual(makeWellFormed('"'), '"', + self.assertEqual(make_well_formed('"'), '"', 'Incorrectly translated unquoted double quote') - self.assertEqual(makeWellFormed("'", '"'), "'", + self.assertEqual(make_well_formed("'", XML_ENTS_ESCAPE_QUOT), "'", 'Incorrectly translated double-quoted single quote') - self.assertEqual(makeWellFormed('"', "'"), '"', + self.assertEqual(make_well_formed('"', XML_ENTS_ESCAPE_APOS), '"', 'Incorrectly translated single-quoted double quote') - self.assertEqual(makeWellFormed("'", "'"), ''', + self.assertEqual(make_well_formed("'", XML_ENTS_ESCAPE_APOS), ''', 'Incorrectly translated single-quoted single quote') - self.assertEqual(makeWellFormed('"', '"'), '"', + self.assertEqual(make_well_formed('"', XML_ENTS_ESCAPE_QUOT), '"', 'Incorrectly translated double-quoted double quote') From 9656569a72a8a46cbf5465f43515f9fc38bfecea Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Thu, 21 May 2020 12:15:32 +0000 Subject: [PATCH 235/270] serializeXML: Refactor the attribute ordering code Rewrite the code for ordering attributes in the output and extract it into a function. As a side-effect, we ensure we only use the `.item(index)` method once per attribute because it is inefficient (see https://bugs.python.org/issue40689). Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 62 +++++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 15de2bd..473a2ed 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3433,6 +3433,42 @@ TEXT_CONTENT_ELEMENTS = ['text', 'tspan', 'tref', 'textPath', 'altGlyph', 'flowDiv', 'flowPara', 'flowSpan', 'flowTref', 'flowLine'] +KNOWN_ATTRS = [ + # TODO: Maybe update with full list from https://www.w3.org/TR/SVG/attindex.html + # (but should be kept intuitively ordered) + 'id', 'xml: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', + ] + +KNOWN_ATTRS_ORDER_BY_NAME = defaultdict(lambda: len(KNOWN_ATTRS), + {name: order for order, name in enumerate(KNOWN_ATTRS)}) + + +# use custom order for known attributes and alphabetical order for the rest +def _attribute_sort_key_function(attribute): + name = attribute.name + order_value = KNOWN_ATTRS_ORDER_BY_NAME[name] + return order_value, name + + +def attributes_ordered_for_output(element): + if not element.hasAttributes(): + return [] + attribute = element.attributes + # The .item(i) call is painfully slow (bpo#40689). Therefore we ensure we + # call it at most once per attribute. + # - it would be many times faster to use `attribute.values()` but sadly + # that is an "experimental" interface. + return sorted((attribute.item(i) for i in range(attribute.length)), + key=_attribute_sort_key_function) + + # hand-rolled serialization function that has the following benefits: # - pretty printing # - somewhat judicious use of whitespace @@ -3453,30 +3489,8 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): outParts.extend([(indent_type * indent_depth), '<', element.nodeName]) # 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', 'xml: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)] - for index in attrIndices: - attr = attrList.item(index) - + attrs = attributes_ordered_for_output(element) + for attr in attrs: attrValue = attr.nodeValue quote, xml_ent = choose_quote_character(attrValue) attrValue = make_well_formed(attrValue, xml_ent) From 397ffc55297c8a56b7c25a5f9c3eddd2d78723fe Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Thu, 21 May 2020 12:40:04 +0000 Subject: [PATCH 236/270] make_well_formed: Optimize for the common case of nothing needs to be escaped Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index 473a2ed..18a81d2 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3413,6 +3413,11 @@ def remapNamespacePrefix(node, oldprefix, newprefix): def make_well_formed(text, quote_dict=None): if quote_dict is None: quote_dict = XML_ENTS_NO_QUOTES + if not any(c in text for c in quote_dict): + # The quote-able characters are quite rare in SVG (they mostly only + # occur in text elements in practice). Therefore it make sense to + # optimize for this common case + return text return ''.join(quote_dict[c] if c in quote_dict else c for c in text) From 5881890e44d8c24c184a7516609d8b752180469e Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Mon, 18 May 2020 20:46:43 +0000 Subject: [PATCH 237/270] removeUnreferencedElements: Remove defs before unref elements The `removeUnusedDefs` function can take `referencedIDs` as parameter and its work do not invalidate it. By moving it up in `removeUnreferencedElements` we can save a call to `findReferencedElements` per call to `removeUnreferencedElements`. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 18a81d2..c9eff0e 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -674,6 +674,16 @@ def removeUnreferencedElements(doc, keepDefs): identifiedElements = findElementsWithId(doc.documentElement) referencedIDs = findReferencedElements(doc.documentElement) + if not keepDefs: + # Remove most unreferenced elements inside defs + defs = doc.documentElement.getElementsByTagName('defs') + for aDef in defs: + elemsToRemove = removeUnusedDefs(doc, aDef, referencedIDs=referencedIDs) + for elem in elemsToRemove: + elem.parentNode.removeChild(elem) + _num_elements_removed += 1 + num += 1 + for id in identifiedElements: if id not in referencedIDs: goner = identifiedElements[id] @@ -684,15 +694,6 @@ def removeUnreferencedElements(doc, keepDefs): num += 1 _num_elements_removed += 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) - _num_elements_removed += 1 - num += 1 return num From c5362743c3d582943ca4d2cc10a6fba8d9d6b3c6 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Mon, 18 May 2020 21:04:20 +0000 Subject: [PATCH 238/270] _getStyle: Avoid calling getAttribute twice for no reason _getStyle accounted for ~8.9% (~17700) of all calls to getAttribute on devices/hidef/secure-card.svgz file from the Oxygen icon theme. This commit removes this part of the dead weight. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index c9eff0e..1bb4980 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1610,9 +1610,12 @@ def removeDuplicateGradients(doc): def _getStyle(node): u"""Returns the style attribute of a node as a dictionary.""" - if node.nodeType == Node.ELEMENT_NODE and len(node.getAttribute('style')) > 0: + if node.nodeType != Node.ELEMENT_NODE: + return {} + style_attribute = node.getAttribute('style') + if style_attribute: styleMap = {} - rawStyles = node.getAttribute('style').split(';') + rawStyles = style_attribute.split(';') for style in rawStyles: propval = style.split(':') if len(propval) == 2: From 528ad91418a4d705bc1d1d1af9e946057217e05e Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Thu, 21 May 2020 11:26:10 +0000 Subject: [PATCH 239/270] removeUnusedDefs: Call getAttribute at most once per element Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 1bb4980..78ae200 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -647,8 +647,12 @@ def removeUnusedDefs(doc, defElem, elemsToRemove=None, referencedIDs=None): 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 == Node.ELEMENT_NODE and (elem.getAttribute('id') == '' or - elem.getAttribute('id') not in referencedIDs): + if elem.nodeType != Node.ELEMENT_NODE: + continue + + elem_id = elem.getAttribute('id') + + if elem_id == '' or elem_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']: From 29a7474f746b2807058385ca6a6d9cf33812f4b2 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 19 May 2020 21:56:15 +0000 Subject: [PATCH 240/270] removeNamespacedAttributes: Avoid calling it twice as it is indempotent Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 78ae200..6bb1b8f 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -955,7 +955,6 @@ def removeUnreferencedIDs(referencedIDs, identifiedElements): def removeNamespacedAttributes(node, namespaces): - global _num_attributes_removed num = 0 if node.nodeType == Node.ELEMENT_NODE: # remove all namespace'd attributes from this element @@ -966,9 +965,8 @@ def removeNamespacedAttributes(node, namespaces): if attr is not None and attr.namespaceURI in namespaces: attrsToRemove.append(attr.nodeName) for attrName in attrsToRemove: - num += 1 - _num_attributes_removed += 1 node.removeAttribute(attrName) + num += len(attrsToRemove) # now recurse for children for child in node.childNodes: @@ -3656,8 +3654,8 @@ def scourString(in_string, options=None): if options.keep_editor_data is False: while removeNamespacedElements(doc.documentElement, unwanted_ns) > 0: pass - while removeNamespacedAttributes(doc.documentElement, unwanted_ns) > 0: - pass + _num_attributes_removed += removeNamespacedAttributes(doc.documentElement, + unwanted_ns) # remove the xmlns: declarations now xmlnsDeclsToRemove = [] From 045f1f0ad543e7bd30b724255161f21c568f1a8f Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 19 May 2020 21:59:02 +0000 Subject: [PATCH 241/270] removeNamespacedElements: Avoid calling it twice as it is indempotent Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 6bb1b8f..d03e5d0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -975,7 +975,6 @@ def removeNamespacedAttributes(node, namespaces): def removeNamespacedElements(node, namespaces): - global _num_elements_removed num = 0 if node.nodeType == Node.ELEMENT_NODE: # remove all namespace'd child nodes from this element @@ -985,9 +984,8 @@ def removeNamespacedElements(node, namespaces): if child is not None and child.namespaceURI in namespaces: childrenToRemove.append(child) for child in childrenToRemove: - num += 1 - _num_elements_removed += 1 node.removeChild(child) + num += len(childrenToRemove) # now recurse for children for child in node.childNodes: @@ -3652,8 +3650,8 @@ def scourString(in_string, options=None): # 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 is False: - while removeNamespacedElements(doc.documentElement, unwanted_ns) > 0: - pass + _num_elements_removed += removeNamespacedElements(doc.documentElement, + unwanted_ns) _num_attributes_removed += removeNamespacedAttributes(doc.documentElement, unwanted_ns) From fd2daf44b4f4cbad899889426ca644fff6696c29 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Thu, 21 May 2020 13:00:47 +0000 Subject: [PATCH 242/270] Avoid compiling "the same" regex multiple times Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index d03e5d0..ec5441a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -80,6 +80,9 @@ XML_ENTS_ESCAPE_APOS["'"] = ''' XML_ENTS_ESCAPE_QUOT = XML_ENTS_NO_QUOTES.copy() XML_ENTS_ESCAPE_QUOT['"'] = '"' +# Used to split values where "x y" or "x,y" or a mix of the two is allowed +RE_COMMA_WSP = re.compile(r"\s*[\s,]\s*") + NS = {'SVG': 'http://www.w3.org/2000/svg', 'XLINK': 'http://www.w3.org/1999/xlink', 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', @@ -2738,7 +2741,7 @@ def parseListOfPoints(s): # 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()) + ws_nums = RE_COMMA_WSP.split(s.strip()) nums = [] # also, if 100-100 is found, split it into two also @@ -3351,7 +3354,7 @@ def properlySizeDoc(docElement, options): # else we have a statically sized image and we should try to remedy that # parse viewBox attribute - vbSep = re.split('[, ]+', docElement.getAttribute('viewBox')) + vbSep = RE_COMMA_WSP.split(docElement.getAttribute('viewBox')) # if we have a valid viewBox we need to check it vbWidth, vbHeight = 0, 0 if len(vbSep) == 4: @@ -3810,7 +3813,7 @@ def scourString(in_string, options=None): elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) viewBox = doc.documentElement.getAttribute('viewBox') if viewBox: - lengths = re.split('[, ]+', viewBox) + lengths = RE_COMMA_WSP.split(viewBox) lengths = [scourUnitlessLength(length) for length in lengths] doc.documentElement.setAttribute('viewBox', ' '.join(lengths)) From 985cb58a262f34f793c68d89a696feeb16611414 Mon Sep 17 00:00:00 2001 From: Patrick Storz <eduard.braun2@gmx.de> Date: Mon, 8 Jun 2020 19:45:48 +0200 Subject: [PATCH 243/270] Remove outdated comment originally added in 879300373f39319c76ef58bd10ce0e32ee55d22b and fixed shortly after in 2dc788aa3f5196a14c00262238ba838248a48077 --- scour/scour.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index ec5441a..5ba03fd 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3649,9 +3649,7 @@ def scourString(in_string, options=None): # 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? + # remove unneeded namespaced elements/attributes added by common editors if options.keep_editor_data is False: _num_elements_removed += removeNamespacedElements(doc.documentElement, unwanted_ns) From ace24df5c3245a556581839f7cbbd6fe92b214f4 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Mon, 18 May 2020 21:00:55 +0000 Subject: [PATCH 244/270] removeDuplicateGradients: Avoid compiling regex unless we need it Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 5ba03fd..97ec672 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1581,11 +1581,11 @@ def removeDuplicateGradients(doc): # 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") # With --keep-unreferenced-defs, we can end up with # unreferenced gradients. See GH#156. if dup_id in referencedIDs: + funcIRI = re.compile('url\\([\'"]?#' + dup_id + '[\'"]?\\)') # matches url(#a), url('#a') and url("#a") for elem in referencedIDs[dup_id]: # find out which attribute referenced the duplicate gradient for attr in ['fill', 'stroke']: From 9e3a5f2e40c39877bcda9a877bd65dac19aba829 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 19 May 2020 18:02:25 +0000 Subject: [PATCH 245/270] removeDuplicateGradients: Refactor how duplicates are passed around This commit is mostly to enable the following commit to make improvements. It does reduce the number of duplicate getAttribute calls by a tiny bit but it is unlikely to matter in practice. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 97ec672..1b9c150 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1536,7 +1536,7 @@ def removeDuplicateGradients(doc): global _num_elements_removed num = 0 - gradientsToRemove = {} + gradients_to_remove = [] for gradType in ['linearGradient', 'radialGradient']: grads = doc.getElementsByTagName(gradType) @@ -1553,35 +1553,35 @@ def removeDuplicateGradients(doc): continue master = bucket[0] duplicates = bucket[1:] + duplicates_ids = [d.getAttribute('id') for d in duplicates] master_id = master.getAttribute('id') if not master_id: # If our selected "master" copy does not have an ID, # then replace it with one that does (assuming any of # them has one). This avoids broken images like we # saw in GH#203 - for i in range(len(duplicates)): - dup = duplicates[i] - dup_id = dup.getAttribute('id') + for i in range(len(duplicates_ids)): + dup_id = duplicates_ids[i] if dup_id: + # We do not bother updating the master field + # as it is not used any more. + master_id = duplicates_ids[i] duplicates[i] = master - master = dup + # Clear the old id to avoid a redundant remapping + duplicates_ids[i] = "" break - gradientsToRemove[master] = duplicates + gradients_to_remove.append((master_id, duplicates_ids, duplicates)) # get a collection of all elements that are referenced and their referencing elements referencedIDs = findReferencedElements(doc.documentElement) - for masterGrad in gradientsToRemove: - master_id = masterGrad.getAttribute('id') - for dupGrad in gradientsToRemove[masterGrad]: + for master_id, duplicates_ids, duplicates in gradients_to_remove: + for dup_id, dupGrad in zip(duplicates_ids, duplicates): # 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') - # With --keep-unreferenced-defs, we can end up with # unreferenced gradients. See GH#156. if dup_id in referencedIDs: From 36ee0932a4595f5a26e3db720672bd9b45dd47bf Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Tue, 19 May 2020 18:04:48 +0000 Subject: [PATCH 246/270] removeDuplicateGradients: Compile at most one regex per master gradient Regex compilation is by far the most expensive part of removeDuplicateGradients. This commit reduces the pain a bit by trading "many small regexes" to "few larger regexes", which avoid some of the compilation overhead. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scour/scour.py b/scour/scour.py index 1b9c150..fb8f9d1 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1576,6 +1576,7 @@ def removeDuplicateGradients(doc): # get a collection of all elements that are referenced and their referencing elements referencedIDs = findReferencedElements(doc.documentElement) for master_id, duplicates_ids, duplicates in gradients_to_remove: + funcIRI = None for dup_id, dupGrad in zip(duplicates_ids, duplicates): # if the duplicate gradient no longer has a parent that means it was # already re-mapped to another master gradient @@ -1585,7 +1586,10 @@ def removeDuplicateGradients(doc): # With --keep-unreferenced-defs, we can end up with # unreferenced gradients. See GH#156. if dup_id in referencedIDs: - funcIRI = re.compile('url\\([\'"]?#' + dup_id + '[\'"]?\\)') # matches url(#a), url('#a') and url("#a") + if funcIRI is None: + # matches url(#<ANY_DUP_ID>), url('#<ANY_DUP_ID>') and url("#<ANY_DUP_ID>") + dup_id_regex = "|".join(duplicates_ids) + funcIRI = re.compile('url\\([\'"]?#(?:' + dup_id_regex + ')[\'"]?\\)') for elem in referencedIDs[dup_id]: # find out which attribute referenced the duplicate gradient for attr in ['fill', 'stroke']: From a3f761f40c16c038f1a43319a900657c19a1208f Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Thu, 21 May 2020 13:44:11 +0000 Subject: [PATCH 247/270] Refactor some code out of removeDuplicateGradients This is commits enables a future optimization (but is not a notable optimization in itself). Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 113 ++++++++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index fb8f9d1..519e06a 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1532,21 +1532,26 @@ def computeGradientBucketKey(grad): return "\x1e".join(subKeys) -def removeDuplicateGradients(doc): - global _num_elements_removed - num = 0 +def detect_duplicate_gradients(*grad_lists): + """Detects duplicate gradients from each iterable/generator given as argument - gradients_to_remove = [] - - for gradType in ['linearGradient', 'radialGradient']: - grads = doc.getElementsByTagName(gradType) - gradBuckets = defaultdict(list) + Yields (master, master_id, duplicates_id, duplicates) tuples where: + * master_id: The ID attribute of the master element. This will always be non-empty + and not None as long at least one of the gradients have a valid ID. + * duplicates_id: List of ID attributes of the duplicate gradients elements (can be + empty where the gradient had no ID attribute) + * duplicates: List of elements that are duplicates of the `master` element. Will + never include the `master` element. Has the same order as `duplicates_id` - i.e. + `duplicates[X].getAttribute("id") == duplicates_id[X]`. + """ + for grads in grad_lists: + grad_buckets = defaultdict(list) for grad in grads: key = computeGradientBucketKey(grad) - gradBuckets[key].append(grad) + grad_buckets[key].append(grad) - for bucket in six.itervalues(gradBuckets): + for bucket in six.itervalues(grad_buckets): if len(bucket) < 2: # The gradient must be unique if it is the only one in # this bucket. @@ -1571,47 +1576,59 @@ def removeDuplicateGradients(doc): duplicates_ids[i] = "" break - gradients_to_remove.append((master_id, duplicates_ids, duplicates)) + yield master_id, duplicates_ids, duplicates + + +def dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids): + func_iri = None + for dup_id, dup_grad in zip(duplicates_ids, duplicates): + # if the duplicate gradient no longer has a parent that means it was + # already re-mapped to another master gradient + if not dup_grad.parentNode: + continue + + # With --keep-unreferenced-defs, we can end up with + # unreferenced gradients. See GH#156. + if dup_id in referenced_ids: + if func_iri is None: + # matches url(#<ANY_DUP_ID>), url('#<ANY_DUP_ID>') and url("#<ANY_DUP_ID>") + dup_id_regex = "|".join(duplicates_ids) + func_iri = re.compile('url\\([\'"]?#(?:' + dup_id_regex + ')[\'"]?\\)') + for elem in referenced_ids[dup_id]: + # find out which attribute referenced the duplicate gradient + for attr in ['fill', 'stroke']: + v = elem.getAttribute(attr) + (v_new, n) = func_iri.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) = func_iri.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 + dup_grad.parentNode.removeChild(dup_grad) + + +def removeDuplicateGradients(doc): + global _num_elements_removed + num = 0 + + linear_gradients = doc.getElementsByTagName('linearGradient') + radial_gradients = doc.getElementsByTagName('radialGradient') # get a collection of all elements that are referenced and their referencing elements referencedIDs = findReferencedElements(doc.documentElement) - for master_id, duplicates_ids, duplicates in gradients_to_remove: - funcIRI = None - for dup_id, dupGrad in zip(duplicates_ids, duplicates): - # 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 - - # With --keep-unreferenced-defs, we can end up with - # unreferenced gradients. See GH#156. - if dup_id in referencedIDs: - if funcIRI is None: - # matches url(#<ANY_DUP_ID>), url('#<ANY_DUP_ID>') and url("#<ANY_DUP_ID>") - dup_id_regex = "|".join(duplicates_ids) - funcIRI = re.compile('url\\([\'"]?#(?:' + dup_id_regex + ')[\'"]?\\)') - for elem in referencedIDs[dup_id]: - # 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) - _num_elements_removed += 1 - num += 1 + for master_id, duplicates_ids, duplicates in detect_duplicate_gradients(linear_gradients, radial_gradients): + dedup_gradient(master_id, duplicates_ids, duplicates, referencedIDs) + _num_elements_removed += len(duplicates) + num += len(duplicates) return num From 0e82b8dcad7b9fcc51327122468ae867f5a6eb6d Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Thu, 21 May 2020 13:53:06 +0000 Subject: [PATCH 248/270] Refactor removeDuplicateGradients to loop until it reaches a fixed point This is commits enables a future optimization (but is not a notable optimization in itself). Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 519e06a..14e17ba 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1617,18 +1617,21 @@ def dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids): def removeDuplicateGradients(doc): - global _num_elements_removed + prev_num = -1 num = 0 - linear_gradients = doc.getElementsByTagName('linearGradient') - radial_gradients = doc.getElementsByTagName('radialGradient') + while prev_num != num: + prev_num = num + + linear_gradients = doc.getElementsByTagName('linearGradient') + radial_gradients = doc.getElementsByTagName('radialGradient') + + # get a collection of all elements that are referenced and their referencing elements + referenced_ids = findReferencedElements(doc.documentElement) + for master_id, duplicates_ids, duplicates in detect_duplicate_gradients(linear_gradients, radial_gradients): + dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids) + num += len(duplicates) - # get a collection of all elements that are referenced and their referencing elements - referencedIDs = findReferencedElements(doc.documentElement) - for master_id, duplicates_ids, duplicates in detect_duplicate_gradients(linear_gradients, radial_gradients): - dedup_gradient(master_id, duplicates_ids, duplicates, referencedIDs) - _num_elements_removed += len(duplicates) - num += len(duplicates) return num @@ -3775,8 +3778,7 @@ def scourString(in_string, options=None): pass # remove duplicate gradients - while removeDuplicateGradients(doc) > 0: - pass + _num_elements_removed += removeDuplicateGradients(doc) if options.group_collapse: _num_elements_removed += mergeSiblingGroupsWithCommonAttributes(doc.documentElement) From 3d29029c721bb6a0ed40c528ed1dc753c4331484 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Mon, 18 May 2020 20:26:21 +0000 Subject: [PATCH 249/270] findReferencedElements: Use a set instead of list for tracking nodes Except for one caller, nothing cares what kind of collection is used. By migrating to a set, we can enable a future rewrite. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 14e17ba..48f7d92 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -556,7 +556,7 @@ def findReferencedElements(node, ids=None): Returns IDs of all referenced elements - node is the node at which to start the search. - returns a map which has the id as key and - each value is is a list of nodes + each value is is a set of nodes Currently looks at 'xlink:href' and all attributes in 'referencingProps' """ @@ -586,9 +586,9 @@ def findReferencedElements(node, ids=None): # we remove the hash mark from the beginning of the id id = href[1:] if id in ids: - ids[id].append(node) + ids[id].add(node) else: - ids[id] = [node] + ids[id] = {node} # now get all style properties and the fill, stroke, filter attributes styles = node.getAttribute('style').split(';') @@ -619,9 +619,9 @@ def findReferencingProperty(node, prop, val, ids): if len(val) >= 7 and val[0:5] == 'url(#': id = val[5:val.find(')')] if id in ids: - ids[id].append(node) + ids[id].add(node) else: - ids[id] = [node] + ids[id] = {node} # if the url has a quote in it, we need to compensate elif len(val) >= 8: id = None @@ -633,9 +633,9 @@ def findReferencingProperty(node, prop, val, ids): id = val[6:val.find("')")] if id is not None: if id in ids: - ids[id].append(node) + ids[id].add(node) else: - ids[id] = [node] + ids[id] = {node} def removeUnusedDefs(doc, defElem, elemsToRemove=None, referencedIDs=None): @@ -1457,7 +1457,7 @@ def collapseSinglyReferencedGradients(doc): elem.namespaceURI == NS['SVG'] ): # found a gradient that is referenced by only 1 other element - refElem = nodes[0] + refElem = nodes.pop() if refElem.nodeType == Node.ELEMENT_NODE and refElem.nodeName in ['linearGradient', 'radialGradient'] \ and refElem.namespaceURI == NS['SVG']: # elem is a gradient referenced by only one other gradient (refElem) From ca2b32c0b38b0d0d224cadf2db12ce9df6339c63 Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Thu, 21 May 2020 14:14:25 +0000 Subject: [PATCH 250/270] removeDuplicateGradients: Maintain referenced_ids This avoids calling `findReferencedElements` more than once per removeDuplicateGradients. This is good for performance as `findReferencedElements` is one of the slowest functions in scour. Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 48f7d92..a3e0c82 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1615,19 +1615,39 @@ def dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids): # it is safe to remove this gradient from the document dup_grad.parentNode.removeChild(dup_grad) + # If the gradients have an ID, we update referenced_ids to match the newly remapped IDs. + # This enable us to avoid calling findReferencedElements once per loop, which is helpful as it is + # one of the slowest functions in scour. + if master_id: + try: + master_references = referenced_ids[master_id] + except KeyError: + master_references = set() + + for dup_id in duplicates_ids: + references = referenced_ids.pop(dup_id, None) + if references is None: + continue + master_references.update(references) + + # Only necessary but needed if the master gradient did + # not have any references originally + referenced_ids[master_id] = master_references + def removeDuplicateGradients(doc): prev_num = -1 num = 0 + # get a collection of all elements that are referenced and their referencing elements + referenced_ids = findReferencedElements(doc.documentElement) + while prev_num != num: prev_num = num linear_gradients = doc.getElementsByTagName('linearGradient') radial_gradients = doc.getElementsByTagName('radialGradient') - # get a collection of all elements that are referenced and their referencing elements - referenced_ids = findReferencedElements(doc.documentElement) for master_id, duplicates_ids, duplicates in detect_duplicate_gradients(linear_gradients, radial_gradients): dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids) num += len(duplicates) From 9a1286132f31a1fec7e908270abe549fdde62a1f Mon Sep 17 00:00:00 2001 From: Niels Thykier <niels@thykier.net> Date: Wed, 10 Jun 2020 18:18:21 +0000 Subject: [PATCH 251/270] remapNamespacePrefix: Preserve prefix of attribute names (#255) Preserve prefix of attribute names when copying them over to the new node. This fixes an unintentional rewrite of `xml:space` to `space` that also caused scour to strip whitespace that should have been preserved. Closes: #239 Signed-off-by: Niels Thykier <niels@thykier.net> --- scour/scour.py | 2 +- test_scour.py | 11 ++++++++++- unittests/redundant-svg-namespace.svg | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index a3e0c82..2afc64d 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3445,7 +3445,7 @@ def remapNamespacePrefix(node, oldprefix, newprefix): attrList = node.attributes for i in range(attrList.length): attr = attrList.item(i) - newNode.setAttributeNS(attr.namespaceURI, attr.localName, attr.nodeValue) + newNode.setAttributeNS(attr.namespaceURI, attr.name, attr.nodeValue) # clone and add all the child nodes for child in node.childNodes: diff --git a/test_scour.py b/test_scour.py index e55b9db..5f44e03 100755 --- a/test_scour.py +++ b/test_scour.py @@ -1502,7 +1502,16 @@ class RemoveRedundantSvgNamespacePrefix(unittest.TestCase): doc = scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement r = doc.getElementsByTagNameNS(SVGNS, 'rect')[1] self.assertEqual(r.tagName, 'rect', - 'Redundant svg: prefix not removed') + 'Redundant svg: prefix not removed from rect') + t = doc.getElementsByTagNameNS(SVGNS, 'text')[0] + self.assertEqual(t.tagName, 'text', + 'Redundant svg: prefix not removed from text') + + # Regression test for #239 + self.assertEqual(t.getAttribute('xml:space'), 'preserve', + 'Required xml: prefix removed in error') + self.assertEqual(t.getAttribute("space"), '', + 'Required xml: prefix removed in error') class RemoveDefaultGradX1Value(unittest.TestCase): diff --git a/unittests/redundant-svg-namespace.svg b/unittests/redundant-svg-namespace.svg index 5022693..1d1dd8d 100644 --- a/unittests/redundant-svg-namespace.svg +++ b/unittests/redundant-svg-namespace.svg @@ -5,4 +5,5 @@ <title>Test + Hallo World From f0788d5c0d49ee93e2aad44654cbc4ab22ffa70b Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Sat, 27 Jun 2020 10:26:05 +0000 Subject: [PATCH 252/270] renameID: Fix bug when swapping two IDs Signed-off-by: Niels Thykier --- scour/scour.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 2afc64d..a5559c6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -847,8 +847,6 @@ def renameID(idFrom, idTo, identifiedElements, referringNodes): definingNode = identifiedElements[idFrom] definingNode.setAttribute("id", idTo) - del identifiedElements[idFrom] - identifiedElements[idTo] = definingNode num += len(idFrom) - len(idTo) # Update references to renamed node From ab97a014274cb620128fb050dd76a5b6663cc478 Mon Sep 17 00:00:00 2001 From: Patrick Storz Date: Thu, 6 Aug 2020 22:34:04 +0200 Subject: [PATCH 253/270] anti-aliasing --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3eb1529..b8fecb0 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The goal of Scour is to output a file that renderes identically at a fraction of Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). Scour was originally developed by Jeff "codedread" Schiller and Louis Simard in in 2010. -The project moved to GitLab in 2013 an is now maintained by Tobias "oberstet" Oberstein and Eduard "Ede_123" Braun. +The project moved to GitLab in 2013 an is now maintained by Tobias "oberstet" Oberstein and Patrick "Ede_123" Storz. ## Installation diff --git a/setup.py b/setup.py index 01a7ae0..cf3ed08 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ Website Authors: - Jeff Schiller, Louis Simard (original authors) - Tobias Oberstein (maintainer) - - Eduard Braun (maintainer) + - Patrick Storz (maintainer) """ VERSIONFILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "scour", "__init__.py") From c84731e12d4af0e219c4389434ed68f6708d8c33 Mon Sep 17 00:00:00 2001 From: Patrick Storz Date: Thu, 6 Aug 2020 22:34:25 +0200 Subject: [PATCH 254/270] Scour v0.38 --- HISTORY.md | 10 ++++++++++ scour/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 3ba52d2..36a4a9f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,15 @@ # Release Notes for Scour +## Version 0.38 (2020-08-06) +* Fix issue with dropping xlink:href attribute when collapsing referenced gradients ([#206](https://github.com/scour-project/scour/pull/206)) +* Fix issue with dropping ID while de-duplicating gradients ([#207](https://github.com/scour-project/scour/pull/207)) +* Improve `--shorten-ids` so it re-maps IDs that are already used in the document if they're shorter ([#187](https://github.com/scour-project/scour/pull/187)) +* Fix whitespace handling for SVG 1.2 flowed text ([#235](https://github.com/scour-project/scour/issues/235)) +* Improvement: Merge sibling `` nodes with identical attributes ([#208](https://github.com/scour-project/scour/pull/208)) +* Improve performance of XML serialization ([#247](https://github.com/scour-project/scour/pull/247)) +* Improve performance of gradient de-duplication ([#248](https://github.com/scour-project/scour/pull/248)) +* Some general performance improvements ([#249](https://github.com/scour-project/scour/pull/249)) + ## Version 0.37 (2018-07-04) * Fix escaping of quotes in attribute values. ([#152](https://github.com/scour-project/scour/pull/152)) * A lot of performance improvements making processing significantly faster in many cases. ([#167](https://github.com/scour-project/scour/pull/167), [#169](https://github.com/scour-project/scour/pull/169), [#171](https://github.com/scour-project/scour/pull/171), [#185](https://github.com/scour-project/scour/pull/185)) diff --git a/scour/__init__.py b/scour/__init__.py index f3d44ee..a64bc69 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ # ############################################################################### -__version__ = u'0.37' +__version__ = u'0.38' From f56843acc003ba3bbf71880d1780a9d94974f67a Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Wed, 2 Sep 2020 17:03:36 +0000 Subject: [PATCH 255/270] mergeSiblingGroupsWithCommonAttributes: Avoid creating "empty" -tags (#261) Closes: #260 Signed-off-by: Niels Thykier --- scour/scour.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scour/scour.py b/scour/scour.py index a5559c6..f462dc8 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1158,6 +1158,9 @@ def mergeSiblingGroupsWithCommonAttributes(elem): i -= 1 continue attributes = {a.nodeName: a.nodeValue for a in currentNode.attributes.values()} + if not attributes: + i -= 1 + continue runStart, runEnd = i, i runElements = 1 while runStart > 0: From 23835da44a11fcdbb877f11870ffc442f268352c Mon Sep 17 00:00:00 2001 From: Patrick Storz Date: Wed, 2 Sep 2020 19:19:01 +0200 Subject: [PATCH 256/270] Scour v0.38.1 --- HISTORY.md | 3 +++ scour/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 36a4a9f..7e9ba07 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,8 @@ # Release Notes for Scour +## Version 0.38.1 (2020-09-02) +* Fix regression caused by new feature to merge sibling groups ([#260](https://github.com/scour-project/scour/issues/260)) + ## Version 0.38 (2020-08-06) * Fix issue with dropping xlink:href attribute when collapsing referenced gradients ([#206](https://github.com/scour-project/scour/pull/206)) * Fix issue with dropping ID while de-duplicating gradients ([#207](https://github.com/scour-project/scour/pull/207)) diff --git a/scour/__init__.py b/scour/__init__.py index a64bc69..bf043c3 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ # ############################################################################### -__version__ = u'0.38' +__version__ = u'0.38.1' From 92e64f0d7f92a3ed235d6ccd8ec360a645b71463 Mon Sep 17 00:00:00 2001 From: Patrick Storz Date: Wed, 2 Sep 2020 19:25:20 +0200 Subject: [PATCH 257/270] CI: test with Python 3.9-dev and 3.10-dev --- .travis.yml | 2 ++ tox.ini | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 991bb30..2342ba0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,8 @@ python: - 3.6 - 3.7 - 3.8 + - 3.9-dev + - 3.10-dev install: - pip install tox-travis codecov diff --git a/tox.ini b/tox.ini index 238864d..82420b6 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,8 @@ envlist = py36 py37 py38 + py39 + py310 flake8 From b8a071f99580ab87ba3838ce9410e0f702b99621 Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Sun, 22 Nov 2020 14:00:43 +0000 Subject: [PATCH 258/270] scour: Fix another variant of the crash from #260 (#264) Signed-off-by: Niels Thykier --- scour/scour.py | 8 ++-- test_scour.py | 6 +++ unittests/group-sibling-merge-crash.svg | 13 +++++++ unittests/group-sibling-merge.svg | 52 ++++++++++++------------- 4 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 unittests/group-sibling-merge-crash.svg diff --git a/scour/scour.py b/scour/scour.py index f462dc8..c987bb0 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1200,11 +1200,11 @@ def mergeSiblingGroupsWithCommonAttributes(elem): for node in nodes: if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: # Merge - primaryGroup.childNodes.extend(node.childNodes) - node.childNodes = [] + for child in node.childNodes[:]: + primaryGroup.appendChild(child) + elem.removeChild(node).unlink() else: - primaryGroup.childNodes.append(node) - elem.childNodes.remove(node) + primaryGroup.appendChild(node) # each child gets the same treatment, recursively for childNode in elem.childNodes: diff --git a/test_scour.py b/test_scour.py index 5f44e03..549333f 100755 --- a/test_scour.py +++ b/test_scour.py @@ -2099,6 +2099,12 @@ class GroupSiblingMerge(unittest.TestCase): self.assertEqual(doc.getElementsByTagName('g').length, 8, 'Sibling merging is disabled by --disable-group-collapsing') + def test_sibling_merge_crash(self): + doc = scourXmlFile('unittests/group-sibling-merge-crash.svg', + parse_args([''])) + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Sibling merge should work without causing crashes') + class GroupCreation(unittest.TestCase): diff --git a/unittests/group-sibling-merge-crash.svg b/unittests/group-sibling-merge-crash.svg new file mode 100644 index 0000000..3e50347 --- /dev/null +++ b/unittests/group-sibling-merge-crash.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/unittests/group-sibling-merge.svg b/unittests/group-sibling-merge.svg index 0a2181c..c7f0d02 100644 --- a/unittests/group-sibling-merge.svg +++ b/unittests/group-sibling-merge.svg @@ -1,29 +1,29 @@ -Produced by GNUPLOT 5.2 patchlevel 8 - - - - -0 - - - - - -5000 - - - - - -10000 - - - - - -15000 - - + Produced by GNUPLOT 5.2 patchlevel 8 + + + + + 0 + + + + + + 5000 + + + + + + 10000 + + + + + + 15000 + + From 04bf3d79a0db6b60cf9e226a8cdb96fc216cd71f Mon Sep 17 00:00:00 2001 From: Patrick Storz Date: Sun, 22 Nov 2020 15:05:13 +0100 Subject: [PATCH 259/270] Scour v0.38.2 --- HISTORY.md | 3 +++ scour/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 7e9ba07..1661d5a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,8 @@ # Release Notes for Scour +## Version 0.38.2 (2020-11-22) +* Fix another regression caused by new feature to merge sibling groups ([#260](https://github.com/scour-project/scour/issues/260)) + ## Version 0.38.1 (2020-09-02) * Fix regression caused by new feature to merge sibling groups ([#260](https://github.com/scour-project/scour/issues/260)) diff --git a/scour/__init__.py b/scour/__init__.py index bf043c3..591803a 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -16,4 +16,4 @@ # ############################################################################### -__version__ = u'0.38.1' +__version__ = u'0.38.2' From 7a83e7148d4132f3b3d27708d585eb12ccd35ba5 Mon Sep 17 00:00:00 2001 From: Patrick Storz Date: Sun, 22 Nov 2020 15:21:13 +0100 Subject: [PATCH 260/270] CI: test with Python 3.9 stable --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2342ba0..b8861da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: - 3.6 - 3.7 - 3.8 - - 3.9-dev + - 3.9 - 3.10-dev install: - pip install tox-travis codecov @@ -22,7 +22,7 @@ matrix: fast_finish: true include: - - python: 3.8 + - python: 3.9 env: - TOXENV=flake8 From aa9796ea87c90d081d1c2892660547fe7e01c807 Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Sun, 17 May 2020 19:04:53 +0000 Subject: [PATCH 261/270] Refactor: Create a g_tag_is_unmergeable Both `mergeSiblingGroupsWithCommonAttributes` and `removeNestedGroups` used the same code in different forms. Extract it into its own function. Signed-off-by: Niels Thykier --- scour/scour.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index c987bb0..63493bd 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1022,6 +1022,15 @@ def removeDescriptiveElements(doc, options): return num +def g_tag_is_unmergeable(node): + """Check if a tag can be merged or not + + tags with a title or descriptions should generally be left alone. + """ + return any(True for n in node.childNodes if n.nodeType == Node.ELEMENT_NODE + and n.nodeName in ('title', 'desc') and n.namespaceURI == NS['SVG']) + + def removeNestedGroups(node): """ This walks further and further down the tree, removing groups @@ -1038,11 +1047,7 @@ def removeNestedGroups(node): 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 == Node.ELEMENT_NODE and grandchild.namespaceURI == NS['SVG'] and \ - grandchild.nodeName in ['title', 'desc']: - break - else: + if not g_tag_is_unmergeable(child): groupsToRemove.append(child) for g in groupsToRemove: @@ -1169,11 +1174,7 @@ def mergeSiblingGroupsWithCommonAttributes(elem): if nextNode.nodeName != 'g' or nextNode.namespaceURI != NS['SVG']: break nextAttributes = {a.nodeName: a.nodeValue for a in nextNode.attributes.values()} - hasNoMergeTags = (True for n in nextNode.childNodes - if n.nodeType == Node.ELEMENT_NODE - and n.nodeName in ('title', 'desc') - and n.namespaceURI == NS['SVG']) - if attributes != nextAttributes or any(hasNoMergeTags): + if attributes != nextAttributes or g_tag_is_unmergeable(nextNode): break else: runElements += 1 From 7b9c4ee93508d08042cf4b910bcdcc47c71645ab Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Sun, 10 Jan 2021 11:35:06 +0000 Subject: [PATCH 262/270] Simplif loop logic Signed-off-by: Niels Thykier --- scour/scour.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 63493bd..70cba9b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1381,12 +1381,11 @@ def removeUnusedAttributesOnParent(elem): 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] + for child in childElements: inheritedAttrs = [] for name in unusedAttrs: val = child.getAttribute(name) - if val == '' or val is None or val == 'inherit': + if val == '' or val == 'inherit': inheritedAttrs.append(name) for a in inheritedAttrs: del unusedAttrs[a] From a7a16799a21f124c4041d65c7dcd01f4a4c672ed Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Sun, 10 Jan 2021 13:54:52 +0000 Subject: [PATCH 263/270] Remove some dead assignments Signed-off-by: Niels Thykier --- scour/scour.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 70cba9b..fe6fcd2 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -3401,7 +3401,6 @@ def properlySizeDoc(docElement, options): # parse viewBox attribute vbSep = RE_COMMA_WSP.split(docElement.getAttribute('viewBox')) # 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 @@ -3436,7 +3435,6 @@ def remapNamespacePrefix(node, oldprefix, newprefix): parent = node.parentNode # create a replacement node - newNode = None if newprefix != '': newNode = doc.createElementNS(namespace, newprefix + ":" + localName) else: From 68c1e545dae30e7ab84837f4e3d267026485ea3d Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Sun, 10 Jan 2021 13:52:26 +0000 Subject: [PATCH 264/270] Replace global stats vars with a ScourStats object This enables us to get rid of all the global variables. I used the opportunity to update function names where call sites where affected to move scour a step towards a more pythonic style in general. Signed-off-by: Niels Thykier --- scour/scour.py | 260 +++++++++++++++++++++---------------------------- scour/stats.py | 28 ++++++ 2 files changed, 137 insertions(+), 151 deletions(-) create mode 100644 scour/stats.py diff --git a/scour/scour.py b/scour/scour.py index fe6fcd2..6a69d61 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -63,6 +63,7 @@ from decimal import Context, Decimal, InvalidOperation, getcontext import six from six.moves import range, urllib +from scour.stats import ScourStats from scour.svg_regex import svg_parser from scour.svg_transform import svg_transform_parser from scour.yocto_css import parseCssString @@ -666,14 +667,13 @@ def removeUnusedDefs(doc, defElem, elemsToRemove=None, referencedIDs=None): return elemsToRemove -def removeUnreferencedElements(doc, keepDefs): +def remove_unreferenced_elements(doc, keepDefs, stats): """ 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 _num_elements_removed num = 0 # Remove certain unreferenced elements outside of defs @@ -688,8 +688,8 @@ def removeUnreferencedElements(doc, keepDefs): elemsToRemove = removeUnusedDefs(doc, aDef, referencedIDs=referencedIDs) for elem in elemsToRemove: elem.parentNode.removeChild(elem) - _num_elements_removed += 1 - num += 1 + stats.num_elements_removed += len(elemsToRemove) + num += len(elemsToRemove) for id in identifiedElements: if id not in referencedIDs: @@ -699,7 +699,7 @@ def removeUnreferencedElements(doc, keepDefs): and goner.parentNode.tagName != 'defs'): goner.parentNode.removeChild(goner) num += 1 - _num_elements_removed += 1 + stats.num_elements_removed += 1 return num @@ -937,20 +937,18 @@ def unprotected_ids(doc, options): return identifiedElements -def removeUnreferencedIDs(referencedIDs, identifiedElements): +def remove_unreferenced_ids(referencedIDs, identifiedElements): """ Removes the unreferenced ID attributes. Returns the number of ID attributes removed """ - global _num_ids_removed keepTags = ['font'] num = 0 for id in identifiedElements: node = identifiedElements[id] if id not in referencedIDs and node.nodeName not in keepTags: node.removeAttribute('id') - _num_ids_removed += 1 num += 1 return num @@ -994,7 +992,7 @@ def removeNamespacedElements(node, namespaces): return num -def removeDescriptiveElements(doc, options): +def remove_descriptive_elements(doc, options): elementTypes = [] if options.remove_descriptive_elements: elementTypes.extend(("title", "desc", "metadata")) @@ -1006,20 +1004,16 @@ def removeDescriptiveElements(doc, options): if options.remove_metadata: elementTypes.append("metadata") if not elementTypes: - return + return 0 - global _num_elements_removed - num = 0 elementsToRemove = [] for elementType in elementTypes: elementsToRemove.extend(doc.documentElement.getElementsByTagName(elementType)) for element in elementsToRemove: element.parentNode.removeChild(element) - num += 1 - _num_elements_removed += 1 - return num + return len(elementsToRemove) def g_tag_is_unmergeable(node): @@ -1031,13 +1025,12 @@ def g_tag_is_unmergeable(node): and n.nodeName in ('title', 'desc') and n.namespaceURI == NS['SVG']) -def removeNestedGroups(node): +def remove_nested_groups(node, stats): """ 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 _num_elements_removed num = 0 groupsToRemove = [] @@ -1054,13 +1047,14 @@ def removeNestedGroups(node): while g.childNodes.length > 0: g.parentNode.insertBefore(g.firstChild, g) g.parentNode.removeChild(g) - _num_elements_removed += 1 - num += 1 + + num += len(groupsToRemove) + stats.num_elements_removed += len(groupsToRemove) # now recurse for children for child in node.childNodes: if child.nodeType == Node.ELEMENT_NODE: - num += removeNestedGroups(child) + num += remove_nested_groups(child, stats) return num @@ -1215,7 +1209,7 @@ def mergeSiblingGroupsWithCommonAttributes(elem): return num -def createGroupsForCommonAttributes(elem): +def create_groups_for_common_attributes(elem, stats): """ Creates elements to contain runs of 3 or more consecutive child elements having at least one common attribute. @@ -1227,8 +1221,6 @@ def createGroupsForCommonAttributes(elem): This function acts recursively on the given element. """ - num = 0 - global _num_elements_removed # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement # could be added here @@ -1328,9 +1320,8 @@ def createGroupsForCommonAttributes(elem): # Include the group in elem's children. elem.childNodes.insert(runStart, group) group.parentNode = elem - num += 1 curChild = runStart - 1 - _num_elements_removed -= 1 + stats.num_elements_removed -= 1 else: curChild -= 1 else: @@ -1339,9 +1330,7 @@ def createGroupsForCommonAttributes(elem): # each child gets the same treatment, recursively for childNode in elem.childNodes: if childNode.nodeType == Node.ELEMENT_NODE: - num += createGroupsForCommonAttributes(childNode) - - return num + create_groups_for_common_attributes(childNode, stats) def removeUnusedAttributesOnParent(elem): @@ -1398,8 +1387,7 @@ def removeUnusedAttributesOnParent(elem): return num -def removeDuplicateGradientStops(doc): - global _num_elements_removed +def remove_duplicate_gradient_stops(doc, stats): num = 0 for gradType in ['linearGradient', 'radialGradient']: @@ -1432,15 +1420,13 @@ def removeDuplicateGradientStops(doc): for stop in stopsToRemove: stop.parentNode.removeChild(stop) - num += 1 - _num_elements_removed += 1 + num += len(stopsToRemove) + stats.num_elements_removed += len(stopsToRemove) - # linear gradients return num -def collapseSinglyReferencedGradients(doc): - global _num_elements_removed +def collapse_singly_referenced_gradients(doc, stats): num = 0 identifiedElements = findElementsWithId(doc.documentElement) @@ -1502,8 +1488,9 @@ def collapseSinglyReferencedGradients(doc): # now delete elem elem.parentNode.removeChild(elem) - _num_elements_removed += 1 + stats.num_elements_removed += 1 num += 1 + return num @@ -2277,12 +2264,10 @@ def convertColors(element): # reusing data structures, etc -def cleanPath(element, options): +def clean_path(element, options, stats): """ Cleans the path string (d attribute) of the element """ - global _num_bytes_saved_in_path_data - global _num_path_segments_removed # this gets the parser object from svg_regex.py oldPathStr = element.getAttribute('d') @@ -2433,34 +2418,34 @@ def cleanPath(element, options): while i < len(data): if data[i] == data[i + 1] == 0: del data[i:i + 2] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 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] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: i += 6 elif cmd == 'a': while i < len(data): if data[i + 5] == data[i + 6] == 0: del data[i:i + 7] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 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] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: i += 4 elif cmd in ['h', 'v']: oldLen = len(data) path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) - _num_path_segments_removed += len(path[pathIndex][1]) - oldLen + stats.num_path_segments_removed += len(path[pathIndex][1]) - oldLen # remove no-op commands pathIndex = len(path) @@ -2481,7 +2466,7 @@ def cleanPath(element, options): # continue a draw on the same subpath after a # "z"). del path[pathIndex] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: # it is not safe to rewrite "m0 0 ..." to "l..." # because of this "z" command. @@ -2491,7 +2476,7 @@ def cleanPath(element, options): # Ends with an empty move (but no line/draw # following it) del path[pathIndex] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 continue if subpath_needs_anchor: subpath_needs_anchor = False @@ -2499,7 +2484,7 @@ def cleanPath(element, options): # unanchored, i.e. we can replace "m0 0 ..." with # "l..." as there is no "z" after it. path[pathIndex] = ('l', data[2:]) - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 # fixup: Delete subcommands having no coordinates. path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] @@ -2587,14 +2572,14 @@ def cleanPath(element, options): lineTuples = [] # append the v and then the remaining line coords newPath.append(('v', [data[i + 1]])) - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 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]])) - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: lineTuples.extend(data[i:i + 2]) i += 2 @@ -2614,7 +2599,7 @@ def cleanPath(element, options): cmd = 'l' # dealing with linetos now # append the v and then the remaining line coords newPath.append(('v', [data[i + 1]])) - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 elif data[i + 1] == 0: if lineTuples: # flush the m/l command, then append the h and then the remaining line coords @@ -2622,7 +2607,7 @@ def cleanPath(element, options): lineTuples = [] cmd = 'l' # dealing with linetos now newPath.append(('h', [data[i]])) - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: lineTuples.extend(data[i:i + 2]) i += 2 @@ -2651,7 +2636,7 @@ def cleanPath(element, options): curveTuples = [] # append the s command newPath.append(('s', [data[i + 2], data[i + 3], data[i + 4], data[i + 5]])) - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: j = 0 while j <= 5: @@ -2676,7 +2661,7 @@ def cleanPath(element, options): curveTuples = [] # append the t command newPath.append(('t', [data[i + 2], data[i + 3]])) - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: j = 0 while j <= 3: @@ -2709,7 +2694,7 @@ def cleanPath(element, options): if is_same_sign(data[coordIndex], data[coordIndex+1]): data[coordIndex] += data[coordIndex+1] del data[coordIndex+1] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: coordIndex += 1 @@ -2722,7 +2707,7 @@ def cleanPath(element, options): data[coordIndex+1] += data[coordIndex+3] del data[coordIndex+2] # delete the next two elements del data[coordIndex+2] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: coordIndex += 2 @@ -2735,7 +2720,7 @@ def cleanPath(element, options): data[coordIndex+1] += data[coordIndex+3] del data[coordIndex+2] # delete the next two elements del data[coordIndex+2] - _num_path_segments_removed += 1 + stats.num_path_segments_removed += 1 else: coordIndex += 2 @@ -2770,7 +2755,7 @@ def cleanPath(element, 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): - _num_bytes_saved_in_path_data += (len(oldPathStr) - len(newPathStr)) + stats.num_bytes_saved_in_path_data += (len(oldPathStr) - len(newPathStr)) element.setAttribute('d', newPathStr) @@ -2834,11 +2819,11 @@ def parseListOfPoints(s): return nums -def cleanPolygon(elem, options): +def clean_polygon(elem, options): """ Remove unnecessary closing point of polygon points attribute """ - global _num_points_removed_from_polygon + num_points_removed_from_polygon = 0 pts = parseListOfPoints(elem.getAttribute('points')) N = len(pts) / 2 @@ -2847,8 +2832,9 @@ def cleanPolygon(elem, options): (endx, endy) = pts[-2:] if startx == endx and starty == endy: del pts[-2:] - _num_points_removed_from_polygon += 1 + num_points_removed_from_polygon += 1 elem.setAttribute('points', scourCoordinates(pts, options, True)) + return num_points_removed_from_polygon def cleanPolyline(elem, options): @@ -3288,31 +3274,27 @@ def optimizeTransforms(element, options): return num -def removeComments(element): +def remove_comments(element, stats): """ Removes comments from the element and its children. """ - global _num_bytes_saved_in_comments - num = 0 if isinstance(element, xml.dom.minidom.Comment): - _num_bytes_saved_in_comments += len(element.data) + stats.num_bytes_saved_in_comments += len(element.data) + stats.num_comments_removed += 1 element.parentNode.removeChild(element) - num += 1 else: for subelement in element.childNodes[:]: - num += removeComments(subelement) - - return num + remove_comments(subelement, stats) -def embedRasters(element, options): +def embed_rasters(element, options): import base64 """ Converts raster references to inline images. NOTE: there are size limits to base64-encoding handling in browsers """ - global _num_rasters_embedded + num_rasters_embedded = 0 href = element.getAttributeNS(NS['XLINK'], 'href') @@ -3380,8 +3362,9 @@ def embedRasters(element, options): element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster.decode()) - _num_rasters_embedded += 1 + num_rasters_embedded += 1 del b64eRaster + return num_rasters_embedded def properlySizeDoc(docElement, options): @@ -3629,10 +3612,14 @@ def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): # 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): +def scourString(in_string, options=None, stats=None): # sanitize options (take missing attributes from defaults, discard unknown attributes) options = sanitizeOptions(options) + if stats is None: + # This is easier than doing "if stats is not None:" checks all over the place + stats = ScourStats() + # default or invalid value if(options.cdigits < 0): options.cdigits = options.digits @@ -3645,37 +3632,6 @@ def scourString(in_string, options=None): scouringContext = Context(prec=options.digits) scouringContextC = Context(prec=options.cdigits) - # globals for tracking statistics - # TODO: get rid of these globals... - global _num_elements_removed - global _num_attributes_removed - global _num_ids_removed - global _num_comments_removed - global _num_style_properties_fixed - global _num_rasters_embedded - global _num_path_segments_removed - global _num_points_removed_from_polygon - global _num_bytes_saved_in_path_data - global _num_bytes_saved_in_colors - global _num_bytes_saved_in_comments - global _num_bytes_saved_in_ids - global _num_bytes_saved_in_lengths - global _num_bytes_saved_in_transforms - _num_elements_removed = 0 - _num_attributes_removed = 0 - _num_ids_removed = 0 - _num_comments_removed = 0 - _num_style_properties_fixed = 0 - _num_rasters_embedded = 0 - _num_path_segments_removed = 0 - _num_points_removed_from_polygon = 0 - _num_bytes_saved_in_path_data = 0 - _num_bytes_saved_in_colors = 0 - _num_bytes_saved_in_comments = 0 - _num_bytes_saved_in_ids = 0 - _num_bytes_saved_in_lengths = 0 - _num_bytes_saved_in_transforms = 0 - doc = xml.dom.minidom.parseString(in_string) # determine number of flowRoot elements in input document @@ -3690,14 +3646,14 @@ def scourString(in_string, options=None): print("WARNING: {}".format(errmsg), file=sys.stderr) # remove descriptive elements - removeDescriptiveElements(doc, options) + stats.num_elements_removed += remove_descriptive_elements(doc, options) # remove unneeded namespaced elements/attributes added by common editors if options.keep_editor_data is False: - _num_elements_removed += removeNamespacedElements(doc.documentElement, - unwanted_ns) - _num_attributes_removed += removeNamespacedAttributes(doc.documentElement, - unwanted_ns) + stats.num_elements_removed += removeNamespacedElements(doc.documentElement, + unwanted_ns) + stats.num_attributes_removed += removeNamespacedAttributes(doc.documentElement, + unwanted_ns) # remove the xmlns: declarations now xmlnsDeclsToRemove = [] @@ -3708,7 +3664,7 @@ def scourString(in_string, options=None): for attr in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attr) - _num_attributes_removed += 1 + stats.num_attributes_removed += len(xmlnsDeclsToRemove) # ensure namespace for SVG is declared # TODO: what if the default namespace is something else (i.e. some valid namespace)? @@ -3743,28 +3699,28 @@ def scourString(in_string, options=None): for attrName in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attrName) - _num_attributes_removed += 1 + stats.num_attributes_removed += len(xmlnsDeclsToRemove) for prefix in redundantPrefixes: remapNamespacePrefix(doc.documentElement, prefix, '') if options.strip_comments: - _num_comments_removed = removeComments(doc) + remove_comments(doc, stats) if options.strip_xml_space_attribute and doc.documentElement.hasAttribute('xml:space'): doc.documentElement.removeAttribute('xml:space') - _num_attributes_removed += 1 + stats.num_attributes_removed += 1 # repair style (remove unnecessary style properties and change them into XML attributes) - _num_style_properties_fixed = repairStyle(doc.documentElement, options) + stats.num_style_properties_fixed = repairStyle(doc.documentElement, options) # convert colors to #RRGGBB format if options.simple_colors: - _num_bytes_saved_in_colors = convertColors(doc.documentElement) + stats.num_bytes_saved_in_colors = convertColors(doc.documentElement) # remove unreferenced gradients/patterns outside of defs # and most unreferenced elements inside of defs - while removeUnreferencedElements(doc, options.keep_defs) > 0: + while remove_unreferenced_elements(doc, options.keep_defs, stats) > 0: pass # remove empty defs, metadata, g @@ -3782,29 +3738,30 @@ def scourString(in_string, options=None): removeElem = True if removeElem: elem.parentNode.removeChild(elem) - _num_elements_removed += 1 + stats.num_elements_removed += 1 if options.strip_ids: referencedIDs = findReferencedElements(doc.documentElement) identifiedElements = unprotected_ids(doc, options) - removeUnreferencedIDs(referencedIDs, identifiedElements) + stats.num_ids_removed += remove_unreferenced_ids(referencedIDs, + identifiedElements) - while removeDuplicateGradientStops(doc) > 0: + while remove_duplicate_gradient_stops(doc, stats) > 0: pass # remove gradients that are only referenced by one other gradient - while collapseSinglyReferencedGradients(doc) > 0: + while collapse_singly_referenced_gradients(doc, stats) > 0: pass # remove duplicate gradients - _num_elements_removed += removeDuplicateGradients(doc) + stats.num_elements_removed += removeDuplicateGradients(doc) if options.group_collapse: - _num_elements_removed += mergeSiblingGroupsWithCommonAttributes(doc.documentElement) + stats.num_elements_removed += mergeSiblingGroupsWithCommonAttributes(doc.documentElement) # create elements if there are runs of elements with the same attributes. # this MUST be before moveCommonAttributesToParentGroup. if options.group_create: - createGroupsForCommonAttributes(doc.documentElement) + create_groups_for_common_attributes(doc.documentElement, stats) # move common attributes to parent group # NOTE: the if the element's immediate children @@ -3813,20 +3770,20 @@ def scourString(in_string, options=None): # doesn't accept fill=, stroke= etc.! referencedIds = findReferencedElements(doc.documentElement) for child in doc.documentElement.childNodes: - _num_attributes_removed += moveCommonAttributesToParentGroup(child, referencedIds) + stats.num_attributes_removed += moveCommonAttributesToParentGroup(child, referencedIds) # remove unused attributes from parent - _num_attributes_removed += removeUnusedAttributesOnParent(doc.documentElement) + stats.num_attributes_removed += 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: + while remove_nested_groups(doc.documentElement, stats) > 0: pass # remove unnecessary closing point of polygons and scour points for polygon in doc.documentElement.getElementsByTagName('polygon'): - cleanPolygon(polygon, options) + stats.num_points_removed_from_polygon += clean_polygon(polygon, options) # scour points of polyline for polyline in doc.documentElement.getElementsByTagName('polyline'): @@ -3837,11 +3794,11 @@ def scourString(in_string, options=None): if elem.getAttribute('d') == '': elem.parentNode.removeChild(elem) else: - cleanPath(elem, options) + clean_path(elem, options, stats) # shorten ID names as much as possible if options.shorten_ids: - _num_bytes_saved_in_ids += shortenIDs(doc, options.shorten_ids_prefix, options) + stats.num_bytes_saved_in_ids += shortenIDs(doc, options.shorten_ids_prefix, options) # scour lengths (including coordinates) for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', @@ -3858,18 +3815,18 @@ def scourString(in_string, options=None): doc.documentElement.setAttribute('viewBox', ' '.join(lengths)) # more length scouring in this function - _num_bytes_saved_in_lengths = reducePrecision(doc.documentElement) + stats.num_bytes_saved_in_lengths = reducePrecision(doc.documentElement) # remove default values of attributes - _num_attributes_removed += removeDefaultAttributeValues(doc.documentElement, options) + stats.num_attributes_removed += removeDefaultAttributeValues(doc.documentElement, options) # reduce the length of transformation attributes - _num_bytes_saved_in_transforms = optimizeTransforms(doc.documentElement, options) + stats.num_bytes_saved_in_transforms = 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) + stats.num_rasters_embedded += embed_rasters(elem, options) # properly size the SVG document (ideally width/height should be 100% with a viewBox) if options.enable_viewboxing: @@ -3903,7 +3860,7 @@ def scourString(in_string, options=None): # used mostly by unit tests # input is a filename # returns the minidom doc representation of the SVG -def scourXmlFile(filename, options=None): +def scourXmlFile(filename, options=None, stats=None): # sanitize options (take missing attributes from defaults, discard unknown attributes) options = sanitizeOptions(options) # we need to make sure infilename is set correctly (otherwise relative references in the SVG won't work) @@ -3912,7 +3869,7 @@ def scourXmlFile(filename, options=None): # open the file and scour it with open(filename, "rb") as f: in_string = f.read() - out_string = scourString(in_string, options) + out_string = scourString(in_string, options, stats=stats) # prepare the output xml.dom.minidom object doc = xml.dom.minidom.parseString(out_string.encode('utf-8')) @@ -4156,22 +4113,22 @@ def getInOut(options): return [infile, outfile] -def getReport(): +def generate_report(stats): return ( - ' Number of elements removed: ' + str(_num_elements_removed) + os.linesep + - ' Number of attributes removed: ' + str(_num_attributes_removed) + os.linesep + - ' Number of unreferenced IDs removed: ' + str(_num_ids_removed) + os.linesep + - ' Number of comments removed: ' + str(_num_comments_removed) + os.linesep + - ' Number of style properties fixed: ' + str(_num_style_properties_fixed) + os.linesep + - ' Number of raster images embedded: ' + str(_num_rasters_embedded) + os.linesep + - ' Number of path segments reduced/removed: ' + str(_num_path_segments_removed) + os.linesep + - ' Number of points removed from polygons: ' + str(_num_points_removed_from_polygon) + os.linesep + - ' Number of bytes saved in path data: ' + str(_num_bytes_saved_in_path_data) + os.linesep + - ' Number of bytes saved in colors: ' + str(_num_bytes_saved_in_colors) + os.linesep + - ' Number of bytes saved in comments: ' + str(_num_bytes_saved_in_comments) + os.linesep + - ' Number of bytes saved in IDs: ' + str(_num_bytes_saved_in_ids) + os.linesep + - ' Number of bytes saved in lengths: ' + str(_num_bytes_saved_in_lengths) + os.linesep + - ' Number of bytes saved in transformations: ' + str(_num_bytes_saved_in_transforms) + ' Number of elements removed: ' + str(stats.num_elements_removed) + os.linesep + + ' Number of attributes removed: ' + str(stats.num_attributes_removed) + os.linesep + + ' Number of unreferenced IDs removed: ' + str(stats.num_ids_removed) + os.linesep + + ' Number of comments removed: ' + str(stats.num_comments_removed) + os.linesep + + ' Number of style properties fixed: ' + str(stats.num_style_properties_fixed) + os.linesep + + ' Number of raster images embedded: ' + str(stats.num_rasters_embedded) + os.linesep + + ' Number of path segments reduced/removed: ' + str(stats.num_path_segments_removed) + os.linesep + + ' Number of points removed from polygons: ' + str(stats.num_points_removed_from_polygon) + os.linesep + + ' Number of bytes saved in path data: ' + str(stats.num_bytes_saved_in_path_data) + os.linesep + + ' Number of bytes saved in colors: ' + str(stats.num_bytes_saved_in_colors) + os.linesep + + ' Number of bytes saved in comments: ' + str(stats.num_bytes_saved_in_comments) + os.linesep + + ' Number of bytes saved in IDs: ' + str(stats.num_bytes_saved_in_ids) + os.linesep + + ' Number of bytes saved in lengths: ' + str(stats.num_bytes_saved_in_lengths) + os.linesep + + ' Number of bytes saved in transformations: ' + str(stats.num_bytes_saved_in_transforms) ) @@ -4180,10 +4137,11 @@ def start(options, input, output): options = sanitizeOptions(options) start = time.time() + stats = ScourStats() # do the work in_string = input.read() - out_string = scourString(in_string, options).encode("UTF-8") + out_string = scourString(in_string, options, stats=stats).encode("UTF-8") output.write(out_string) # Close input and output files (but do not attempt to close stdin/stdout!) @@ -4209,7 +4167,7 @@ def start(options, input, output): oldsize, sizediff), file=options.ensure_value("stdout", sys.stdout)) if options.verbose: - print(getReport(), file=options.ensure_value("stdout", sys.stdout)) + print(generate_report(stats), file=options.ensure_value("stdout", sys.stdout)) def run(): diff --git a/scour/stats.py b/scour/stats.py new file mode 100644 index 0000000..2762b92 --- /dev/null +++ b/scour/stats.py @@ -0,0 +1,28 @@ +class ScourStats(object): + + __slots__ = ( + 'num_elements_removed', + 'num_attributes_removed', + 'num_style_properties_fixed', + 'num_bytes_saved_in_colors', + 'num_ids_removed', + 'num_comments_removed', + 'num_style_properties_fixed', + 'num_rasters_embedded', + 'num_path_segments_removed', + 'num_points_removed_from_polygon', + 'num_bytes_saved_in_path_data', + 'num_bytes_saved_in_colors', + 'num_bytes_saved_in_comments', + 'num_bytes_saved_in_ids', + 'num_bytes_saved_in_lengths', + 'num_bytes_saved_in_transforms', + ) + + def __init__(self): + self.reset() + + def reset(self): + # Set all stats to 0 + for attr in self.__slots__: + setattr(self, attr, 0) From 841ad54e7f073ebe00950871928f9b77b4d6357b Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Tue, 23 Feb 2021 16:19:52 +0000 Subject: [PATCH 265/270] Refactor function to avoid double negative Signed-off-by: Niels Thykier --- scour/scour.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 6a69d61..9e45a2d 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1016,13 +1016,18 @@ def remove_descriptive_elements(doc, options): return len(elementsToRemove) -def g_tag_is_unmergeable(node): +def g_tag_is_mergeable(node): """Check if a tag can be merged or not tags with a title or descriptions should generally be left alone. """ - return any(True for n in node.childNodes if n.nodeType == Node.ELEMENT_NODE - and n.nodeName in ('title', 'desc') and n.namespaceURI == NS['SVG']) + if any( + True for n in node.childNodes + if n.nodeType == Node.ELEMENT_NODE and n.nodeName in ('title', 'desc') + and n.namespaceURI == NS['SVG'] + ): + return False + return True def remove_nested_groups(node, stats): @@ -1040,7 +1045,7 @@ def remove_nested_groups(node, stats): 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, - if not g_tag_is_unmergeable(child): + if g_tag_is_mergeable(child): groupsToRemove.append(child) for g in groupsToRemove: @@ -1168,7 +1173,7 @@ def mergeSiblingGroupsWithCommonAttributes(elem): if nextNode.nodeName != 'g' or nextNode.namespaceURI != NS['SVG']: break nextAttributes = {a.nodeName: a.nodeValue for a in nextNode.attributes.values()} - if attributes != nextAttributes or g_tag_is_unmergeable(nextNode): + if attributes != nextAttributes or not g_tag_is_mergeable(nextNode): break else: runElements += 1 From fbf0c06e845b585f6ee3bfe5a7470772ab7d86d3 Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Tue, 23 Feb 2021 16:53:21 +0000 Subject: [PATCH 266/270] Avoid mutating a mutable kwarg Signed-off-by: Niels Thykier --- scour/scour.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 9e45a2d..91326c6 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2109,7 +2109,7 @@ def removeDefaultAttributeValue(node, attribute): """ Removes the DefaultAttribute 'attribute' from 'node' if specified conditions are fulfilled - Warning: Does NOT check if the attribute is actually valid for the passed element type for increased preformance! + Warning: Does NOT check if the attribute is actually valid for the passed element type for increased performance! """ if not node.hasAttribute(attribute.name): return 0 @@ -2134,7 +2134,7 @@ def removeDefaultAttributeValue(node, attribute): return 0 -def removeDefaultAttributeValues(node, options, tainted=set()): +def removeDefaultAttributeValues(node, options, tainted=None): u"""'tainted' keeps a set of attributes defined in parent nodes. For such attributes, we don't delete attributes with default values.""" @@ -2142,6 +2142,9 @@ def removeDefaultAttributeValues(node, options, tainted=set()): if node.nodeType != Node.ELEMENT_NODE: return 0 + if tainted is None: + tainted = set() + # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) # # For increased performance do not iterate the whole list for each element but run only on valid subsets From 897e3f565c13fb27b1c19ece860269f3ba70e9fd Mon Sep 17 00:00:00 2001 From: Wolfgang Bangerth Date: Thu, 22 Apr 2021 10:59:44 -0600 Subject: [PATCH 267/270] Minor language edits in README.md. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8fecb0..660ee5c 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ --- -Scour is an SVG optimizer/cleaner that reduces the size of scalable vector graphics by optimizing structure and removing unnecessary data written in Python. +Scour is an SVG optimizer/cleaner written in Python that reduces the size of scalable vector graphics by optimizing structure and removing unnecessary data. It can be used to create streamlined vector graphics suitable for web deployment, publishing/sharing or further processing. -The goal of Scour is to output a file that renderes identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more agressive cleaning. +The goal of Scour is to output a file that renders identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more agressive cleaning. Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). From 85f4b49d59073025cbb3d7ca8a7ba175d18c047c Mon Sep 17 00:00:00 2001 From: "David H. Gutteridge" Date: Sun, 18 Jul 2021 13:56:15 -0400 Subject: [PATCH 268/270] Fix a typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 660ee5c..ace6711 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Scour is an SVG optimizer/cleaner written in Python that reduces the size of sca It can be used to create streamlined vector graphics suitable for web deployment, publishing/sharing or further processing. -The goal of Scour is to output a file that renders identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more agressive cleaning. +The goal of Scour is to output a file that renders identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more aggressive cleaning. Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). From 0609c596766ec98e4e2092b49bd03b802702ba1a Mon Sep 17 00:00:00 2001 From: a1346054 <36859588+a1346054@users.noreply.github.com> Date: Mon, 30 Aug 2021 17:17:00 +0000 Subject: [PATCH 269/270] Fix spelling (#284) --- CONTRIBUTING.md | 4 ++-- HISTORY.md | 12 ++++++------ scour/scour.py | 6 +++--- setup.py | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b34dbf1..96cb109 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Contributions to Scour are welcome, feel free to create a pull request! In order to be able to merge your PR as fast as possible please try to stick to the following guidelines. -> _**TL;DR** (if you now what you're doing) – Always run [`make check`](https://github.com/scour-project/scour/blob/master/Makefile) before creating a PR to check for common problems._ +> _**TL;DR** (if you now what you're doing) – Always run [`make check`](https://github.com/scour-project/scour/blob/master/Makefile) before creating a PR to check for common problems._ ## Code Style @@ -32,4 +32,4 @@ To ensure that all possible code conditions are covered by a test you can use [` make coverage ``` -These reports are also created automatically by our TravisCI builds and are accessible via [Codecov](https://codecov.io/gh/scour-project/scour) \ No newline at end of file +These reports are also created automatically by our TravisCI builds and are accessible via [Codecov](https://codecov.io/gh/scour-project/scour) diff --git a/HISTORY.md b/HISTORY.md index 1661d5a..de0b503 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -47,7 +47,7 @@ ## Version 0.35 (2016-09-14) * Drop official support for Python 2.6. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 2.6 and should find/fix any compatibility issues pull requests are welcome, though.) -* Fix: Unused IDs were not shortended when `--shorten-ids` was used. ([#19](https://github.com/scour-project/scour/issues/62)) +* Fix: Unused IDs were not shortened when `--shorten-ids` was used. ([#19](https://github.com/scour-project/scour/issues/62)) * Fix: Most elements were still removed from `` when `--keep-unreferenced-defs` was used. ([#62](https://github.com/scour-project/scour/issues/62)) * Improve escaping of single/double quotes ('/") in attributes. ([#64](https://github.com/scour-project/scour/issues/64)) * Print usage information if no input file was specified (and no data is available from `stdin`). ([#65](https://github.com/scour-project/scour/issues/65)) @@ -56,13 +56,13 @@ * Improve code to remove default attribute values and add a lot of new default values. ([#70](https://github.com/scour-project/scour/issues/70)) * Fix: Only attempt to group elements that the content model allows to be children of a `` when `--create-groups` is specified. ([#98](https://github.com/scour-project/scour/issues/98)) * Fix: Update list of SVG presentation attributes allowing more styles to be converted to attributes and remove two entries (`line-height` and `visibility`) that were actually invalid. ([#99](https://github.com/scour-project/scour/issues/99)) -* Add three options that work analoguous to `--remove-metadata` (removes `` elements) ([#102](https://github.com/scour-project/scour/issues/102)) +* Add three options that work analogous to `--remove-metadata` (removes `` elements) ([#102](https://github.com/scour-project/scour/issues/102)) * `--remove-titles` (removes `` elements) * `--remove-descriptions` (removes `` elements) * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. ``, `<desc>` and `<metadata>`) * Fix removal rules for the `overflow` attribute. ([#104](https://github.com/scour-project/scour/issues/104)) * Improvement: Automatically order all attributes ([#105](https://github.com/scour-project/scour/issues/105)), as well as `style` declarations ([#107](https://github.com/scour-project/scour/issues/107)) allowing for a constant output across multiple runs of Scour. Before order could change arbitrarily. -* Improve path scouring. ([#108](https://github.com/scour-project/scour/issues/108))<br>Notably Scour performs all caculations with enhanced precision now, guaranteeing maximum accuracy when optimizing path data. Numerical precision is reduced as a last step of the optimization according to the `--precision` option. +* Improve path scouring. ([#108](https://github.com/scour-project/scour/issues/108))<br>Notably Scour performs all calculations with enhanced precision now, guaranteeing maximum accuracy when optimizing path data. Numerical precision is reduced as a last step of the optimization according to the `--precision` option. * Fix replacement of removed duplicate gradients if the `fill`/`stroke` properties contained a fallback. ([#109](https://github.com/scour-project/scour/issues/109)) * Fix conversion of cubic BĂ©zier "curveto" commands into "shorthand/smooth curveto" commands. ([#110](https://github.com/scour-project/scour/issues/110)) * Fix some issues due to removal of properties without considering inheritance rules. ([#111](https://github.com/scour-project/scour/issues/111)) @@ -74,7 +74,7 @@ * Input/output file can now be specified as positional arguments (e.g. `scour input.svg output.svg`). ([#46](https://github.com/scour-project/scour/issues/46)) * Improve `--help` output by intuitively arranging options in groups. ([#46](https://github.com/scour-project/scour/issues/46)) * Add option `--error-on-flowtext` to raise an exception whenever a non-standard `<flowText>` element is found (which is only supported in Inkscape). If this option is not specified a warning will be shown. ([#53](https://github.com/scour-project/scour/issues/53)) -* Automate tests with continouous integration via Travis. ([#52](https://github.com/scour-project/scour/issues/52)) +* Automate tests with continuous integration via Travis. ([#52](https://github.com/scour-project/scour/issues/52)) ## Version 0.33 (2016-01-29) @@ -90,7 +90,7 @@ * Fix a potential regex matching issue in `points` attribute of `<polygon>` and `<polyline>` elements. ([#24](https://github.com/scour-project/scour/issues/24)) * Fix a crash with `points` attribute of `<polygon>` and `<polyline>` starting with a negative number. ([#24](https://github.com/scour-project/scour/issues/24)) * Fix encoding issues when input file contained unicode characters. ([#27](https://github.com/scour-project/scour/issues/27)) -* Fix encoding issues when using `stding`/`stdout` as input/output. ([#27](https://github.com/scour-project/scour/issues/27)) +* Fix encoding issues when using `stding`/`stdout` as input/output. ([#27](https://github.com/scour-project/scour/issues/27)) * Fix removal of comments. If a node contained multiple comments usually not all of them were removed. ([#28](https://github.com/scour-project/scour/issues/28)) @@ -104,7 +104,7 @@ ## Version 0.30 (2014-08-05) -* Fix ingoring of additional args when invoked from scons. +* Fix ignoring of additional args when invoked from scons. ## Version 0.29 (2014-07-26) diff --git a/scour/scour.py b/scour/scour.py index 91326c6..9d19906 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1484,7 +1484,7 @@ def collapse_singly_referenced_gradients(doc, stats): if target_href: # If the elem node had an xlink:href, then the # refElem have to point to it as well to - # perserve the semantics of the image. + # preserve the semantics of the image. refElem.setAttributeNS(NS['XLINK'], 'href', target_href) else: # The elem node had no xlink:href reference, @@ -1883,7 +1883,7 @@ def styleInheritedByChild(node, style, nodeIsChild=False): '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 + # in all other cases we have to assume the inherited value of 'style' is meaningful and has to be kept # (e.g nodes without children at the end of the DOM tree, text nodes, ...) return True @@ -2925,7 +2925,7 @@ def scourCoordinates(data, options, force_whitespace=False, control_points=[], f # - this number starts with a dot but the previous number had *no* dot or exponent # i.e. '1.3 0.5' -> '1.3.5' or '1e3 0.5' -> '1e3.5' is fine but '123 0.5' -> '123.5' is obviously not # - 'force_whitespace' is explicitly set to 'True' - # we never need a space after flags (occuring in elliptical arcs), but librsvg struggles without it + # we never need a space after flags (occurring in elliptical arcs), but librsvg struggles without it if (c > 0 and (force_whitespace or scouredCoord[0].isdigit() diff --git a/setup.py b/setup.py index cf3ed08..990b596 100644 --- a/setup.py +++ b/setup.py @@ -28,10 +28,10 @@ vector graphics by optimizing structure and removing unnecessary data. It can be used to create streamlined vector graphics suitable for web deployment, publishing/sharing or further processing. -The goal of Scour is to output a file that renderes identically at a +The goal of Scour is to output a file that renders identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can -be tweaked for more agressive cleaning. +be tweaked for more aggressive cleaning. Website - http://www.codedread.com/scour/ (original website) From a124f9f415bcadfd13ad52ce0e50b092393c3bec Mon Sep 17 00:00:00 2001 From: Marcus Lindvall <marcus.lindvall@lindvallskaffe.se> Date: Tue, 31 Jan 2023 11:02:40 +0100 Subject: [PATCH 270/270] NonSci output option and InkScape extension added by Aleon --- Makefile | 12 ++-- README.md | 19 ++++-- extension/output_scour.inx | 132 +++++++++++++++++++++++++++++++++++++ extension/output_scour.py | 100 ++++++++++++++++++++++++++++ extension/output_scour.svg | 5 ++ scour/scour.py | 13 +++- 6 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 extension/output_scour.inx create mode 100644 extension/output_scour.py create mode 100644 extension/output_scour.svg diff --git a/Makefile b/Makefile index 09389b5..532618a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: clean install install: - python setup.py install + python3 setup.py install clean: rm -rf build @@ -14,21 +14,21 @@ clean: find . -name "*__pycache__" -type d -prune -exec rm -rf {} \; publish: clean - python setup.py register - python setup.py sdist upload + python3 setup.py register + python3 setup.py sdist upload check: test flake8 test: - python test_scour.py + python3 test_scour.py test_version: - PYTHONPATH=. python -m scour.scour --version + PYTHONPATH=. python3 -m scour.scour --version test_help: - PYTHONPATH=. python -m scour.scour --help + PYTHONPATH=. python3 -m scour.scour --help flake8: flake8 --max-line-length=119 diff --git a/README.md b/README.md index ace6711..c5c0bc8 100644 --- a/README.md +++ b/README.md @@ -18,20 +18,22 @@ Scour is open-source and licensed under [Apache License 2.0](https://github.com/ Scour was originally developed by Jeff "codedread" Schiller and Louis Simard in in 2010. The project moved to GitLab in 2013 an is now maintained by Tobias "oberstet" Oberstein and Patrick "Ede_123" Storz. +This fork was created by Alexander Olsson ([alex@aleon.se](mailto:alex@aleon.se?subject=Scour)) at Aleon Apps. + ## Installation Scour requires [Python](https://www.python.org) 2.7 or 3.4+. Further, for installation, [pip](https://pip.pypa.io) should be used. -To install the [latest release](https://pypi.python.org/pypi/scour) of Scour from PyPI: +To install this fork: ```console -pip install scour +sudo make ``` -To install the [latest trunk](https://github.com/codedread/scour) version (which might be broken!) from GitHub: - +## Extension +Place the modified extension files in the Inkscape extension directory ```console -pip install https://github.com/codedread/scour/archive/master.zip +sudo cp extension/* /usr/share/inkscape/extensions/ ``` ## Usage @@ -61,3 +63,10 @@ Maximum scrubbing and a compressed SVGZ file: scour -i input.svg -o output.svgz --enable-viewboxing --enable-id-stripping \ --enable-comment-stripping --shorten-ids --indent=none ``` + +Remove scientific notation from path data: + +```console +scour -i input.svg -o output.svgz --nonsci-output +``` + diff --git a/extension/output_scour.inx b/extension/output_scour.inx new file mode 100644 index 0000000..b6ce893 --- /dev/null +++ b/extension/output_scour.inx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8"?> +<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> + <name>Optimized SVG Output</name> + <id>org.inkscape.output.scour_inkscape</id> + + <param name="tab" type="notebook"> + <page name="Options" gui-text="Options"> + <param gui-text="Number of significant digits for coordinates:" + gui-description="Specifies the number of significant digits that should be output for coordinates. Note that significant digits are *not* the number of decimals but the overall number of digits in the output. For example if a value of "3" is specified, the coordinate 3.14159 is output as 3.14 while the coordinate 123.675 is output as 124." + name="set-precision" type="int" min="1">5</param> + <spacer/> + <param gui-text="Shorten color values" + gui-description="Convert all color specifications to #RRGGBB (or #RGB where applicable) format." + name="simplify-colors" type="bool">true</param> + <param gui-text="Convert CSS attributes to XML attributes" + gui-description="Convert styles from style tags and inline style="" declarations into XML attributes." + name="style-to-xml" type="bool">true</param> + <spacer/> + <param gui-text="Collapse groups" + gui-description="Remove useless groups, promoting their contents up one level. Requires "Remove unused IDs" to be set." + name="group-collapsing" type="bool">true</param> + <param gui-text="Create groups for similar attributes" + gui-description="Create groups for runs of elements having at least one attribute in common (e.g. fill-color, stroke-opacity, ...)." + name="create-groups" type="bool">true</param> + <spacer/> + <param gui-text="Keep editor data" + gui-description="Don't remove editor-specific elements and attributes. Currently supported: Inkscape, Sodipodi and Adobe Illustrator." + name="keep-editor-data" type="bool">false</param> + <param gui-text="Remove scientific notation" + gui-description="Remove scientific notation from path data." + name="nonsci-output" type="bool">false</param> + <param gui-text="Keep unreferenced definitions" + gui-description="Keep element definitions that are not currently used in the SVG" + name="keep-unreferenced-defs" type="bool">false</param> + <spacer/> + <param gui-text="Work around renderer bugs" + gui-description="Works around some common renderer bugs (mainly libRSVG) at the cost of a slightly larger SVG file." + name="renderer-workaround" type="bool">true</param> + </page> + <page name="Output" gui-text="SVG Output"> + <label appearance="header">Document options</label> + <param gui-text="Remove the XML declaration" + gui-description="Removes the XML declaration (which is optional but should be provided, especially if special characters are used in the document) from the file header." + name="strip-xml-prolog" type="bool">false</param> + <param gui-text="Remove metadata" + gui-description="Remove metadata tags along with all the contained information, which may include license and author information, alternate versions for non-SVG-enabled browsers, etc." + name="remove-metadata" type="bool">false</param> + <param gui-text="Remove comments" + gui-description="Remove all XML comments from output." + name="enable-comment-stripping" type="bool">false</param> + <param gui-text="Embed raster images" + gui-description="Resolve external references to raster images and embed them as Base64-encoded data URLs." + name="embed-rasters" type="bool">true</param> + <param gui-text="Enable viewboxing" + gui-description="Set page size to 100%/100% (full width and height of the display area) and introduce a viewBox specifying the drawings dimensions." + name="enable-viewboxing" type="bool">false</param> + <spacer/> + <label appearance="header">Pretty-printing</label> + <param gui-text="Format output with line-breaks and indentation" + gui-description="Produce nicely formatted output including line-breaks. If you do not intend to hand-edit the SVG file you can disable this option to bring down the file size even more at the cost of clarity." + name="line-breaks" type="bool">true</param> + <param gui-text="Indentation characters:" + gui-description="The type of indentation used for each level of nesting in the output. Specify "None" to disable indentation. This option has no effect if "Format output with line-breaks and indentation" is disabled." + name="indent" type="optiongroup" appearance="combo"> + <option value="space">Space</option> + <option value="tab">Tab</option> + <option context="Indent" value="none">None</option> + </param> + <param gui-text="Depth of indentation:" + gui-description="The depth of the chosen type of indentation. E.g. if you choose "2" every nesting level in the output will be indented by two additional spaces/tabs." + name="nindent" type="int">1</param> + <param gui-text="Strip the "xml:space" attribute from the root SVG element" + gui-description="This is useful if the input file specifies "xml:space='preserve'" in the root SVG element which instructs the SVG editor not to change whitespace in the document at all (and therefore overrides the options above)." + name="strip-xml-space" type="bool">false</param> + </page> + <page name="IDs" gui-text="IDs"> + <param gui-text="Remove unused IDs" + gui-description="Remove all unreferenced IDs from elements. Those are not needed for rendering." + name="enable-id-stripping" type="bool">true</param> + <spacer/> + <param gui-text="Shorten IDs" + gui-description="Minimize the length of IDs using only lowercase letters, assigning the shortest values to the most-referenced elements. For instance, "linearGradient5621" will become "a" if it is the most used element." + name="shorten-ids" type="bool">false</param> + <param gui-text="Prefix shortened IDs with:" + gui-description="Prepend shortened IDs with the specified prefix." + name="shorten-ids-prefix" type="string"></param> + <spacer/> + <param gui-text="Preserve manually created IDs not ending with digits" + gui-description="Descriptive IDs which were manually created to reference or label specific elements or groups (e.g. #arrowStart, #arrowEnd or #textLabels) will be preserved while numbered IDs (as they are generated by most SVG editors including Inkscape) will be removed/shortened." + name="protect-ids-noninkscape" type="bool">true</param> + <param gui-text="Preserve the following IDs:" + gui-description="A comma-separated list of IDs that are to be preserved." + name="protect-ids-list" type="string"></param> + <param gui-text="Preserve IDs starting with:" + gui-description="Preserve all IDs that start with the specified prefix (e.g. specify "flag" to preserve "flag-mx", "flag-pt", etc.)." + name="protect-ids-prefix" type="string"></param> + </page> + <page name="About" gui-text="About"> + <hbox> + <image>output_scour.svg</image> + <spacer/> + <vbox> + <spacer/> + <label>Optimized SVG Output is provided by</label> + <label appearance="header" indent="1">Scour - An SVG Scrubber</label> + <spacer/> + <label>For details please refer to</label> + <label appearance="url" indent="1">https://github.com/scour-project/scour</label> + </vbox> + </hbox> + <spacer size="expand"/> + <hbox> + <label>This version of the extension is designed for</label> + <label>Scour 0.31+</label> + </hbox> + <param name="scour-version" type="string" gui-hidden="true">0.31</param> <!-- this parameter is checked programmatically in the extension to show a warning --> + <param gui-text="Show warnings for older versions of Scour" + name="scour-version-warn-old" type="bool">true</param> + </page> + </param> + + <output> + <extension>.svg</extension> + <mimetype>image/svg+xml</mimetype> + <filetypename>Optimized SVG (*.svg)</filetypename> + <filetypetooltip>Scalable Vector Graphics</filetypetooltip> + </output> + + <script> + <command location="inx" interpreter="python">output_scour.py</command> + </script> +</inkscape-extension> diff --git a/extension/output_scour.py b/extension/output_scour.py new file mode 100644 index 0000000..eebfb8a --- /dev/null +++ b/extension/output_scour.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +""" +Run the scour module on the svg output. +""" + + +import inkex +from inkex.localization import inkex_gettext as _ + +try: + from packaging.version import Version +except ImportError: + raise inkex.DependencyError( + _( + """Failed to import module 'packaging'. +Please make sure it is installed (e.g. using 'pip install packaging' +or 'sudo apt-get install python3-packaging') and try again. +""" + ) + ) + +try: + import scour + from scour.scour import scourString +except ImportError: + raise inkex.DependencyError( + _( + """Failed to import module 'scour'. +Please make sure it is installed (e.g. using 'pip install scour' + or 'sudo apt-get install python3-scour') and try again. +""" + ) + ) + + +class ScourInkscape(inkex.OutputExtension): + """Scour Inkscape Extension""" + + # Scour options + def add_arguments(self, pars): + pars.add_argument("--tab") + pars.add_argument("--simplify-colors", type=inkex.Boolean, dest="simple_colors") + pars.add_argument("--style-to-xml", type=inkex.Boolean) + pars.add_argument( + "--group-collapsing", type=inkex.Boolean, dest="group_collapse" + ) + pars.add_argument("--create-groups", type=inkex.Boolean, dest="group_create") + pars.add_argument("--enable-id-stripping", type=inkex.Boolean, dest="strip_ids") + pars.add_argument("--shorten-ids", type=inkex.Boolean) + pars.add_argument("--shorten-ids-prefix") + pars.add_argument("--embed-rasters", type=inkex.Boolean) + pars.add_argument( + "--keep-unreferenced-defs", type=inkex.Boolean, dest="keep_defs" + ) + pars.add_argument("--keep-editor-data", type=inkex.Boolean) + pars.add_argument("--nonsci-output", type=inkex.Boolean) + pars.add_argument("--remove-metadata", type=inkex.Boolean) + pars.add_argument("--strip-xml-prolog", type=inkex.Boolean) + pars.add_argument("--set-precision", type=int, dest="digits") + pars.add_argument("--indent", dest="indent_type") + pars.add_argument("--nindent", type=int, dest="indent_depth") + pars.add_argument("--line-breaks", type=inkex.Boolean, dest="newlines") + pars.add_argument( + "--strip-xml-space", type=inkex.Boolean, dest="strip_xml_space_attribute" + ) + pars.add_argument("--protect-ids-noninkscape", type=inkex.Boolean) + pars.add_argument("--protect-ids-list") + pars.add_argument("--protect-ids-prefix") + pars.add_argument("--enable-viewboxing", type=inkex.Boolean) + pars.add_argument( + "--enable-comment-stripping", type=inkex.Boolean, dest="strip_comments" + ) + pars.add_argument("--renderer-workaround", type=inkex.Boolean) + + # options for internal use of the extension + pars.add_argument("--scour-version") + pars.add_argument("--scour-version-warn-old", type=inkex.Boolean) + + def save(self, stream): + # version check if enabled in options + if self.options.scour_version_warn_old: + scour_version = scour.__version__ + scour_version_min = self.options.scour_version + if Version(scour_version) < Version(scour_version_min): + raise inkex.AbortExtension( + f""" +The extension 'Optimized SVG Output' is designed for Scour {scour_version_min} or later but you're + using the older version Scour {scour_version}. + +Note: You can permanently disable this message on the 'About' tab of the extension window.""" + ) + del self.options.scour_version + del self.options.scour_version_warn_old + + # do the scouring + stream.write(scourString(self.svg.tostring(), self.options).encode("utf8")) + + +if __name__ == "__main__": + ScourInkscape().run() diff --git a/extension/output_scour.svg b/extension/output_scour.svg new file mode 100644 index 0000000..8f1b941 --- /dev/null +++ b/extension/output_scour.svg @@ -0,0 +1,5 @@ +<svg width="100" height="100" version="1.1" xmlns="http://www.w3.org/2000/svg"> + <path d="m84.5 51.5-12.6 0.504c-8.9e-4 -0.00454-0.00106-0.00914-0.00195-0.0137-0.00104 0.00426-9.21e-4 0.0094-0.00195 0.0137l-15.5 0.623c-0.0092-0.0168-0.016-0.0361-0.0254-0.0527-0.00207 0.0177-0.0019 0.037-0.00391 0.0547l-1.95 0.0781c-0.0314-0.0743-0.0854-0.127-0.186-0.133v0.141l-7.76 0.311c-0.0111-0.0184-0.02-0.0426-0.0312-0.0605 0.00141 0.0201 0.00632 0.0423 0.00781 0.0625l-26.5 1.06-3.74 6.81 2.86 1.36 1.02 1.43 0.887 0.271 1.56 0.41 0.273-0.479 0.953-0.135h2.72l1.29 0.0684 1.22-0.682 0.48 0.525 0.609 0.223 0.887-0.203 1.02-0.205 0.406 0.381 1.3-0.176 1.84-0.273 0.953-0.271 1.02 0.0684 1.09 0.137 0.682 0.0664 0.408-0.477 0.691 0.537 2.1-0.537 1.36 0.273 1.16-0.137 1.8-0.184 0.24-0.361 3.4 0.408 2.18-0.816 1.09 0.408 2.45-0.613h4.46l0.445-0.34 1.02-0.205 0.953 0.137 1.29-0.34 1.34 0.107c0.00197 0.0173 0.00182 0.0397 0.00391 0.0566 0.0223-0.0242 0.0276-0.032 0.0469-0.0527l1.13 0.0918 1.36 0.205 0.387-0.439 2.95 0.234 0.748 0.156 0.584-0.195 1.39-0.369 2.04 0.477 0.848 0.283 1.4-0.623 0.547 0.232 0.543-0.369 5.88 0.369 0.656-0.0957-1.7-9.74zm-69.2 0.297c-0.493-0.0229-0.788 1.14-0.584 1.17 0.186-0.398 0.398-0.77 0.584-1.17zm1.75 0.584c-0.342 0.826-0.871 1.47-1.17 2.34 0.626 0.12 0.267-0.702 0.779-0.195-5e-3 -0.727 0.76-1.69 0.391-2.14zm-0.779 0.193c-0.547 1.33-1.42 2.35-1.95 3.7 1.01-0.744 1.76-2.78 1.95-3.7zm-0.465 0.148c-0.126-0.05-0.42 0.311-0.119 0.631 0.211-0.423 0.195-0.601 0.119-0.631zm-1.87 0.0469c-0.0439 0.426-0.88 1.05-0.391 1.36-0.0281-0.498 0.863-1.06 0.391-1.36zm71.3 0.232c-0.059-0.034-0.0993 0.0425-0.0879 0.352 1.16 2.15 1.33 5.28 2.72 7.2-0.562-2.55-1.39-4.84-2.34-7.01-0.052-0.124-0.202-0.49-0.301-0.547zm-67.6 0.742c-0.835 1.07 0.486 0.719 0 0zm1.27 0.121c-0.0209-0.00292-0.052 0.0184-0.0977 0.0723-0.281 0.726-0.26 0.492-0.584 0 0.151 0.515-0.72 1.51-0.195 1.17 0.0412-0.218 0.215-0.302 0.389-0.389 0.116 0.397-0.365 1.07 0 0.584 0.226-0.169 0.635-1.42 0.488-1.44zm-1.46 1.05c-0.513-0.0589-0.639 0.268-0.584 0.777 0.513 0.0589 0.639-0.268 0.584-0.777zm-2.21 1.29c-0.0245 2.56e-4 -0.0629 0.0204-0.121 0.0664-0.041 0.367-0.782 1.01-0.391 1.17 0.113-0.242 0.683-1.24 0.512-1.24z" fill="#f6e7a1"/> + <path d="m78.9 35.6c-0.411-0.056-0.94 0.163-1.36 0.195-6.84 0.524-14.7 0.699-22.8 1.17-14 0.812-28.8 1.07-41.1 2.14-0.0167 2.54 0.323 3.93 0.195 6.81 0.657 0.901 1.29 1.83 1.56 3.12-1.3 2.27-2.86 4.28-4.28 6.43 0.811 1.33 1.81 2.47 2.73 3.7 1.55-0.0584 1.65 1.33 2.53 1.95 0.809-0.614 1.24-2.09 1.56-2.73 0.662-1.32 1.18-2.69 1.95-3.7-0.412 1.49-0.95 2.35-1.56 3.5-0.827 1.57-2.33 3.59-0.777 4.48 1.15-0.244 1.44 0.493 2.53 1.17 0.109-1.58 1.13-3.28 0.973-4.48-0.574 1.18-0.783 2.72-1.75 3.51 0.872-2.47 1.2-4.42 2.14-6.62 0.211 0.499-0.592 0.93 0 1.17 0.302-0.866 0.392-1.95 1.17-2.34-0.356 3.75-1.28 5.82-2.34 8.96 0.762-0.193 1.13 0.43 1.56 0 0.146-3.74 0.93-6.28 1.56-9.15 0.23 0.381-0.181 1.8-0.195 2.92-0.0045 0.355 0.196 2.8 0 1.17-0.128-1.07-0.163 0.525-0.193 0.779-0.191 1.6-0.63 2.93-0.779 4.48 0.613 0.0939 0.441-0.597 1.17-0.389 0.122-0.592-0.248-1.68 0.195-1.95 0.155 0.754-0.497 2.31 0.779 1.95-0.972-0.939-0.114-2.68-0.584-3.89-0.0436 0.151-0.00608 0.385-0.195 0.391 0.321-1.38-0.0526-3.73 0.584-5.26 0.333 2.26 0.298 5.71 0 7.98 0.701-0.117 0.574 0.594 1.17 0.584 0.371-3.13 0.502-6.51 1.56-8.96 0.322 2.8-1.19 5.9-0.777 9.15 0.757 0.269 2.74 0.24 2.91-0.908 0.00126-0.0852 0.00253-0.171 0.00781-0.26 0.00732 0.0922 4e-3 0.179-0.00781 0.26-0.00558 0.376 0.0493 0.69 0.396 0.713-0.031-1.27 0.26-2.85-0.777-3.12-0.156 0.805 0.706 0.592 0.584 1.36-0.307-0.0176-0.152-0.496-0.584-0.389-0.121 0.529 0.247 1.55-0.195 1.75-0.0605-1.89 0.0118-0.482-0.584 0 0.434-2.81 0.119-6.37 0.973-8.76 0.328 1.95-0.583 4.53 0.195 5.65 0.129-1.95-0.258-4.41 0.195-6.04 0.401 1.03 0.248 2.61 0.584 3.7 0.264-0.948-0.134-2.05 0.389-2.34-0.0319 1.71 0.101 2.41 0.195 3.7 0.153 0.804 0.492-1.81 0.195-2.14 0.878 0.472 0.0244-2.25 0.973-2.34-0.123 1.96-0.463 4.41-0.584 7.59-0.57 0.284-0.947 1.4 0.195 1.36 0.348-3.29 0.287-6.98 1.36-9.54-0.148 3.42-0.457 6.68-1.17 9.54 0.55-0.161 0.506 0.275 0.975 0.195 0.284-0.874-0.131-1.64 0-2.34 0.0441-0.235 0.375-0.192 0.389-0.391-5e-3 0.0753-0.367-0.157-0.389-0.389-0.109-1.19 0.556-1.5 0.584-1.75-0.00214-0.257-0.342-0.179-0.391-0.391 0.956 0.0474-0.0676-1.88 0.584-2.14 0.0329 0.291-0.069 0.719 0.195 0.779 0.397-1.03-0.163-3.02 0.584-3.7 0.076 3.4-0.482 6.49-0.391 9.35-0.257-0.00262-0.177-0.342-0.389-0.391-0.141 0.443-0.175 0.995-0.391 1.36 1.01-0.384 1.56 0.0746 2.53-0.389 0.428-0.553-0.352-1.79-0.389-2.14-0.0591-0.572 0.192-7 0.389-7.01 0.133-0.00477-0.293 2.11 0.195 0.973 0.19-0.442-0.276-1.71 0.195-1.56 0.0429 1.99 0.383 4.16 0.584 5.45 0.197 1.26-0.156 2.78 0.584 3.89 0.0956-0.874-0.0993-1.46-0.195-2.14 0.356 0.0765 0.25 0.258 0.389-0.195 0.494-1.61 0.319-5.87 1.17-7.01 0.0164 2.03-0.489 3.53-0.391 5.64 0.597-0.931 0.19-4.27 0.975-5.26-0.291 2.21 0.367 4.42-0.195 6.23-0.0303 0.0968-0.303-0.37-0.389 0.195-0.163 1.08-0.417 2.54 0.193 2.92 0.53 0.0403 0.365-0.779 0.391-1.17 0.165-2.55 0.42-5.85 1.17-8.18 0.153 2.03-0.506 3.51 0.193 4.87 0.131-2.22-0.28-4.04 0.391-5.45 0.0396 0.368-0.131 1.92 0.195 1.36 0.106-0.283-0.229-1.01 0.193-0.975 0.774 0.411 0.36 2.06-0.193 2.34 0.487-0.0243 0.608-0.373 0.777 0.193 0.109 0.63 0.654 1.26 0.779 2.14 0.218 1.53-0.458 3.66 0.195 4.67 0.116-1.28 0.28-4.49 0.344-6.29-0.053 0.155-0.0894-2.05 0.24-2.08 0.00405-4.77e-4 -0.0488 0.938 0.193 0.777 0.0276-0.751-0.151-1.71 0.391-1.95-0.261 3.24-0.207 6.8-0.975 9.54 0.403-0.116 1.28 0.242 1.36-0.193-0.706-0.975 0.362-2.88-0.195-3.7-0.138 0.705-0.0731 1.61-0.389 2.14 0.24-2.62 0.0955-5.62 0.779-7.79 0.55 0.195 0.488 0.284 0.779 0.389-0.127 3.18 0.0117 6.63-0.779 9.15 1.49 0.29 0.922-1.21 0.973-1.95 0.152-2.23 0.239-5.84 0.779-8.18 0.361 3.38 1.31 6.03 0.779 9.73 1.01-0.558 2.32 0.301 3.7 0.195 1.09-0.0834 1.92-1.06 2.73-0.584 0.111-0.342-0.237-1.15 0.193-1.17 0.424 0.407-0.097 0.557 0 1.17 2.81-0.0119 3.95-0.632 7.21-0.584v-3.51c-0.118-1.01-0.281 0.393-0.195 0.779v1.95c-0.613-0.631-0.145 0.507-0.584 0.584-0.0713-0.523 0.178-2.37-0.195-1.95 0.0272 0.806-0.0531 1.5-0.389 1.95 0.227-3.42 0.271-5.83 1.17-8.57 0.256 0.603-0.577 1.08 0 1.36 0.108-0.347 0.0996-0.808 0.389-0.973 0.214 0.992-0.53 1.73 0.195 2.14 0.127-1.04-0.257-2.59 0.193-3.31 0.162 1.71 0.488 4.13 0.586 6.23 0.0503 1.09-0.572 3.31 0.779 3.12-0.0305-0.866-1.06-1.45-0.391-3.12 0.107 0.283-0.227 1.01 0.195 0.973 0.361-1.5-0.488-2.84-0.584-4.09-0.0388-0.503-0.321-1.56 0.389-1.95-0.109 1.79 0.751 4.14 0.973 6.23 0.452-2.14 0.324-4.87 0.975-6.81 0.167 3.03-0.492 4.41-0.391 7.59-0.766-0.312-0.0675 0.84-0.584 0.777-0.0946-0.23-0.207-0.441-0.389-0.584-0.186 1.97 2.86 1.09 3.31 0.584 0.715 0.577 1.15 0.235 2.34 0.195-0.245-3.96-1.14-5.75-0.779-8.96 0.0117 1.61 0.384 2.86 0.779 4.09-0.0255 0.256-0.0307 1.41 0.193 0.779 0.0245-1.34 0.0891-2.64 0.391-3.7-0.0593 3.12-0.362 5.63-0.391 7.4 1.35 0.649 2.3 0.498 3.51 0.389-0.487-1.91 0.135-4.77 0.389-6.04 0.247-1.23-0.171-2.63 0.584-3.51 0.546 1.55 0.854 3.89 0.975 5.84 0.0355 0.57 0.703 1.52-0.195 1.75-0.081-1.19-0.141-2.91-0.584-4.48 0.2 2.39-0.0405 3.13 0 5.84-0.342-0.28-0.443-0.179-0.389 0.389 0.444 0.619 1.03-0.252 1.56-0.195 0.0956-0.614-0.351-0.686-0.195-1.36 0.316 0.0553 0.305 0.286 0.391-0.195 0.429-2.41-0.329-5.39 0.779-6.62 0.0953 3.83-0.0694 5.11-0.391 8.57 0.783-0.134 1.11 0.187 1.75 0.195 0.431-1.83-0.61-5.63 0-8.18 0.164 0.745 0.255 1.56 0.195 2.53 1.13-0.587-0.379-2.77 0.584-3.31 0.208 3.28-0.0247 6.02-0.389 8.96 0.665-0.0493 0.935-0.494 1.75-0.391-0.324-2.96-0.751-6.09-0.195-8.96 0.349 2.38 1.64 4.85 1.17 7.4-0.286-1.92-0.636-3.78-1.17-5.45 0.308 2.47 0.384 4.63 0.391 7.01 2.58-0.19 4.01 0.398 6.42 0-0.0322-2.86-1.44-5.51-0.584-7.59 0.631 3.03 0.974 4.95 0.779 7.59 1.03-0.0217 1.65-0.39 3.12-0.389-0.406-3.49-2.4-7.07-3.5-10.5 0.471-0.373 1.56-0.123 2.34-0.195 1.74-3.19 0.693-6.95-1.56-9.73-1.31-1.62-5.37-4.62-7.2-4.87zm0.973 0.779c-2.09 0.742-4.43 0.482-6.62 0.584-14.6 0.679-30.9 1.71-45 2.34 10.3-0.00382 20.6-0.925 31-1.36 3.42-0.144 6.89-0.506 10.1-0.389 3.75 0.136-1.31 0.358-1.95 0.389-11.3 0.535-24.1 1.06-35.2 1.56 16.2-0.105 33.1-1.36 48.9-2.14-0.555 0.774-1.59 0.333-2.34 0.391-11.8 0.904-24.2 1.08-35 1.75 13.2 0.0322 25.6-1.19 38.9-1.56-11.2 1.06-23.3 1.14-34.7 1.95 10.8-0.0257 22.1-1.08 33.7-1.36 0.348-0.0551 0.398 0.185 0.193 0.195-1.72-0.00667-2.6 0.106-4.48 0.195 1.9 0.601 4.1-0.178 6.23 0-16.1 1.32-32.5 1.51-48.1 2.72 14.9-0.175 30.6-1.86 45.2-2.14 0.594-0.0114 3.23-0.102 2.34 0.193-15.8 0.773-33 1.47-49.3 2.53 15.5-0.627 29.6-1.22 43.8-1.95 2.27-0.117 4.7-0.654 6.62-0.389 0.334 0.0462 0.411 0.107 0.195 0.195-22.8 1.21-44.8 2.25-67.8 3.31 4.55-0.786 9.43-0.404 14.4-0.973-4.56-0.207-10.1 0.295-14.8 0.584 2.02-0.708 4.94-0.516 7.59-0.584-0.819-0.569-2.47-0.268-3.89-0.195-1.45 0.0739-2.99 0.154-4.09 0.391 0.245-0.759 1.26-0.339 1.75-0.391 7.91-0.827 18.6-0.949 27.5-1.36 9.98-0.464 20.2-1.26 30.2-1.56-16.5 0.356-34.8 1.36-51.2 2.14-0.945 0.0448-2.58 0.597-3.31 0 0.011 0.00882 0.202-0.119-0.193-0.195-1.49-0.286-3.36 0.235-4.87 0.389-0.151-0.753 0.972-0.359 1.17-0.389 7.4-1.14 17.9-0.654 26.5-1.36-0.956-0.526-2.2-0.253-3.31-0.195-8.28 0.434-16.8 0.947-24.3 1.17 2.49-0.689 6.09-0.273 8.76-0.779-1.32-0.218-4.18 0.0249-6.23 0.193 0.562-0.759 1.75-0.343 2.53-0.389 2.34-0.136 5.07-0.341 7.4-0.389-4.46-0.394-8.38 0.32-12.9 0.389 1.47-0.409 2.58-0.499 4.09-0.584 19.9-1.12 40.7-1.51 60.5-2.92zm0.195 0.584c-2.41 0.769-6.07 0.294-8.76 0.779 0.216-0.752 1.79-0.304 2.92-0.391 1.41-0.108 4.16-0.291 5.84-0.389zm-2.92 1.95c-0.861 0.288-1.4-0.265-1.75 0.389 0.63-0.0839 1.68 0.257 1.75-0.389zm-45.2 0.584c-0.723 0.315-1.91 0.16-2.92 0.193-0.331-0.0436-0.482 0.22 0 0.195 0.883-0.22 2.67 0.465 2.92-0.389zm-4.22 0.223c-0.195 0.00524-0.442 0.0981-0.643 0.166h-1.56c-2.37 0.198 1.66 0.235 2.53 0.195 0.00703-0.284-0.135-0.367-0.33-0.361zm19.6 0.125c-0.673-0.00186 0.00284 0.561 0.377 0.041-0.156-0.0281-0.281-0.0408-0.377-0.041zm-0.598 0.041c-7.59 0.391-15.8 0.914-23.6 1.17-0.49 0.016-1.46-0.355-1.75 0.389 5.72-0.283 12.1-0.709 17.5-0.973 2.68-0.13 5.34 0.151 7.79-0.584zm-29.4 0.154c0.318 0.0103 0.294 0.0844-0.605 0.234-0.471 0.0784-0.476 0.266-1.36 0.195-0.0435-0.286 1.44-0.447 1.97-0.43zm66.6 0.408c0.246 0.0191 0.534 0.0775 0.705 0.0215-0.0243 0.43-0.827 0.0835-1.17 0.195 0.0122-0.215 0.219-0.236 0.465-0.217zm-0.854 0.0215c-3.08 0.722-6.23 0.25-9.35 0.779 2.61 0.466 5.3-0.486 8.76-0.195 1.79 0.15-0.0346 0.166-0.391 0.195-7.82 0.652-18.1 0.974-26.5 1.36-12.3 0.572-26.2 0.999-37.6 1.95 16.3-0.727 33.8-1.47 50.8-2.34 3.53-0.179 10.7-1.06 15.8-0.777 2.54 0.14-2.54 0.306-3.7 0.389-20.3 1.45-41.7 1.64-62.5 3.12 0.73 0.54 2.84-0.0633 3.31 0.195-2.28 0.719-3.56 0.183-4.87-1.17 5.37-0.75 10.9-0.938 16.4-1.17 16.5-0.697 33-1.52 49.8-2.34zm1.15 0.367c0.28 0.0203 0.599 0.0814 0.801 0.0234-0.0841 0.435-0.96 0.0775-1.36 0.193 0.0419-0.218 0.283-0.237 0.562-0.217zm-10.7 0.412c-14.7 0.589-27.4 1.3-42.8 1.95-4.36 0.184-8.77 0.295-13.2 0.973 18.2-1.04 37.3-1.57 56.1-2.92zm-38.7 0.193c-3.72 0.333-7.5 0.398-10.9 0.584 2.48 0.442 6.41-0.0578 8.76-0.193 0.819-0.0474 2.08 0.366 2.53-0.391h-0.391zm-15.4 0.00195c-0.568 0.859-2.75 0.11-3.7 0.584 0.201-0.453 2.6-0.385 3.7-0.584zm64.4 0.566c1.14 0.0312 1.69 0.179 0.0918 0.211-1.57 0.0315-4.85 0.365-6.04 0.195 1.26-0.43 3.34-0.0328 4.67-0.389 0.45-0.0242 0.893-0.028 1.27-0.0176zm-51.5 0.211c-2.45 0.0491 0.0431 0.467 1.17 0-0.443 0.0072-0.703-0.0093-1.17 0zm53.7 0.195c-21.5 1.66-43.8 2.03-66.6 3.7 0.766-0.827 2.46-0.5 4.09-0.584 20.1-1.03 40.5-2.13 62.5-3.12zm-13.2 0.195c0.158-0.0076 4.42 0.0822 4.67 0-1.61 0.535-5.47 0.107-7.2 0.193-11.7 0.583-26.9 1.4-40.3 2.14-2.02 0.112-6.48 0.578-7.01 0.195 0.0527 0.0381 4.46-0.316 6.04-0.391 6.14-0.29 12.6-0.649 18.9-0.973 9.15-0.47 17.1-0.797 24.9-1.17zm12.9 0.287c0.248-0.017 0.429 0.0328 0.387 0.297-19.4 0.681-38.6 2.08-58.4 2.92-2.18 0.0918-4.11 0.111-6.23 0.389-0.454 0.0596-0.909 0.322-1.36 0 0.238-0.406 0.949-0.356 1.36-0.389 9.57-0.75 20.2-1.02 29.8-1.56 10.2-0.563 20.6-1.03 31.2-1.56 0.273-0.0138 1.4 0.0067 2.53 0 0.219-0.00143 0.533-0.0846 0.781-0.102zm1.36 0.49c-10 0.993-20.6 1.22-30.8 1.75-9.64 0.505-19.5 0.99-28.8 1.56-0.104-0.771 0.952-0.368 1.17-0.389 8.21-0.77 18.4-0.994 27.1-1.36 10.5-0.446 21-1.14 31.3-1.56zm-0.162 0.574c0.358 9.23e-4 0.673 0.167 0.746 0.789-0.149 0.823-1.14 0.346-1.56 0.389-3.91 0.393-9.9 0.556-14.6 0.779-16.4 0.779-34.7 2.11-50.4 2.34 0.285-1.26 2.49-0.874 3.7-0.973 8.83-0.723 18.9-0.919 28.4-1.36 7.84-0.365 15.7-0.993 24.1-1.36 2.9-0.128 5.88 0.0632 8.57-0.389 0.247-0.0416 0.648-0.206 1.01-0.205zm-3.34 0.594c-0.305 0.815 1.19 0.213 0.391 0-0.0412 0.235-0.217 0.12-0.391 0zm2.14 0c-0.291 0.0331-0.719-0.069-0.779 0.195 0.291-0.0334 0.719 0.069 0.779-0.195zm-58.8 1.75c-0.446 0.765-1.63 0.328-2.53 0.389-0.969 0.066-1.84 0.343-2.73 0.195-1.45-0.15 1.47-0.353 1.95-0.391 0.855-0.0665 2.52-0.0499 3.31-0.193zm60 0.348c0.205 8.05e-4 0.455 0.0127 0.752 0.041-1.47 0.505-2.19-0.0466-0.752-0.041zm-3.85 0.211c0.575 0.0234 1.19 0.0887 1.68 0.0254-0.654 0.45-2.14 0.067-3.12 0.193 0.327-0.225 0.863-0.242 1.44-0.219zm-2.72 0.182c0.188 8.7e-5 0.42 0.00994 0.699 0.0371-1.22 0.509-2.02-0.0377-0.699-0.0371zm-2.05 0.0137c0.346 0.0217 0.732 0.0837 0.996 0.0234-0.208 0.442-1.22 0.075-1.75 0.195 0.104-0.221 0.41-0.24 0.756-0.219zm6.95 0.18c0.605 0.0175 1.17 0.0805 1.64 0.234-2.31 0.0217-4.55 0.118-6.62 0.389 0.622-0.51 2.06-0.534 3.12-0.584 0.616-0.0293 1.26-0.0566 1.86-0.0391zm-10.1 0.0332c0.472-0.00705 0.951-0.0057 1.44 0.00586-2.42 0.249-8.48 0.82-11.1 0.389 3.44 0.0938 6.36-0.345 9.66-0.395zm11.6 0.348c0.0603-0.00636 0.115 0.00558 0.156 0.0469 0.134 0.134-0.768 0.537-0.584 0.195 0.0216-0.0399 0.247-0.223 0.428-0.242zm-8.21 0.0469c0.515-0.0102 1.06 0.0076 1.56 0-1.29 0.461-4.48 0.0586-1.56 0zm7.4 0c0.396 0.72-1.49 0.341-2.34 0.391-1.54 0.0903-3.9 0.385-5.84 0.389h-1.36c-0.849 0.00429-0.0334-0.336 0.389-0.195h1.36c2.39-0.396 5.43-0.153 7.79-0.584zm-24.1 0.188c0.666-0.0157 1.4 0.0365 2.05 0.00781 2.52 0.0686-0.542 0.303-1.56 0.195-0.691 0.117-0.98 0.215-2.14 0.193 0.383-0.298 0.983-0.381 1.65-0.396zm10.6 0.00781c-0.664 0.755-2 0.339-3.12 0.391-0.959 0.0446-2.16 0.369-3.12 0.193 2.25 0.663 7.04-0.279 9.54 0-3.35 0.35-6.93 0.469-10.5 0.584 0.801 0.562 3.53 0.0739 5.06 0 5.06-0.243 11.1-0.328 16.2-0.777-1.05 0.752-2.37 0.339-3.5 0.389-15 0.659-28.2 1.64-43.6 2.34-6.1 0.277-12.9 0.347-17.9 0.973-0.421-0.299-0.997-2.28-0.195-2.53-0.716 0.466 1.17 0.791-0.193 0.777 1.19 0.855 6.22-0.584 7.98 0-0.553-0.184-1.05 0.453-2.92 0.195-1.4 0.357-3.49 0.0133-4.87 0.389 14.6-0.151 29.5-1.48 44.8-1.95-0.832-0.667-4.3 0.156-5.45-0.193 3.21-0.449 7.82-0.311 11.9-0.779zm1.75 0c0.609-0.0098 1.17 0.0074 1.75 0-1.47 0.466-5.05 0.0529-1.75 0zm-15.5 0.146c0.533 0 1.07 0.0806 1.27 0.242-1.05-0.124-1.94 0.216-2.53 0 0.2-0.162 0.733-0.242 1.27-0.242zm-2.71 0.197c0.352-0.00562 0.703 0.00566 1.06 0.0449-3.07 0.511-8.85 0.362-12.1 0.779 2.24-0.724 5.1-0.462 7.79-0.584 1.11-0.0506 2.17-0.223 3.23-0.24zm2.69 0.412c0.51 0.0231 1.06 0.0862 1.48 0.0234-0.525 0.449-1.88 0.0678-2.72 0.193 0.262-0.224 0.732-0.24 1.24-0.217zm-1.44 0.0234c-0.187 0.396-0.748 0.42-1.36 0.389 0.23-0.354 1.02-0.152 1.36-0.389zm-2.47 0.115c0.257-0.0032 0.5 0.0189 0.721 0.0801-0.318-0.0882-1.42 0.159-1.95 0.193-5.7 0.379-12.3 0.975-18.9 0.975-1.77 0 0.453-0.189 0.584-0.195 0.204-0.00953 0.914 0.14 1.17-0.195 4.42-0.0906 10.9-0.347 16-0.584 0.738-0.0341 1.62-0.264 2.39-0.273zm-32.8 0.0137c0.34 0.03 0.424 0.179-0.375 0.26-0.343 0.0348-0.416 0.194-0.779 0.195 0.219-0.396 0.815-0.485 1.15-0.455zm22.4 0.0664c-0.632 0.167-5.16 0.677-6.23 0.193 2.29 0.151 4-0.286 6.23-0.193zm43.2 0.17c0.359 0.00626 0.0638 0.412-0.197 0.414-10.8 1.26-22.4 1.32-33.5 1.95-10.6 0.6-23.1 1.32-32.5 1.36 0.362-0.769 1.36-0.333 1.95-0.389 8.74-0.831 20.5-1.02 30.6-1.56 11-0.588 22.1-0.89 32.3-1.56 0.451-0.0293 0.463-0.287 1.17-0.195 0.08-0.0177 0.146-0.0243 0.197-0.0234zm-22.8 0.0234c-13.7 0.661-27.8 1.54-40.5 2.14-1.18 0.0565-2.63-0.367-3.11 0.389 12.8-0.768 28-1.25 41.1-2.14 0.742-0.0503 1.91 0.379 2.53-0.391zm-31.5 0.162c0.344-0.0091 0.67-2.82e-4 0.973 0.0332-0.834 0.514-2.73 0.512-4.28 0.584-1.88 0.0872-2.52 0.326-3.5 0 1.19 0.394 4.41-0.554 6.81-0.617zm2.51 0.00977c0.542 0.0232 1.12 0.0865 1.58 0.0234-0.589 0.449-2.01 0.0692-2.92 0.195 0.294-0.225 0.797-0.242 1.34-0.219zm-10.7 0.502c0.399-0.0211 0.74 0.0018 0.943 0.105-0.243-0.0584-3.59 0.696-4.09 0.195 0.214 0.214 1.95-0.237 3.15-0.301zm8.54 0.105c-0.775 0.782-2.9 0.218-4.28 0.389 1.14-0.413 3.05-0.0624 4.28-0.389zm-15 0.584c0.43 0.0241 0.0813 0.826 0.193 1.17-0.375-0.0791-0.152-0.755-0.777-0.584-0.142-0.64 0.386-0.298 0.389 0 0.228-0.0312 0.165-0.355 0.195-0.584zm0.664 1.27c0.322 0.0442 1.2 0.44 1.09 0.676-0.593 0.00882-0.757-0.411-1.17-0.584-0.0724-0.0824-0.0271-0.107 0.0801-0.0918zm-4.17 0.0918c-0.187 0.398-0.398 0.77-0.584 1.17-0.204-0.0298 0.0914-1.19 0.584-1.17zm56.7 0.193c0.592 2.99-0.396 6.09-0.389 9.15-1.23-0.33-0.278-1.72-0.195-2.73 0.183-2.23 0.136-4.58 0.584-6.43zm-54.9 0.391c0.37 0.446-0.396 1.41-0.391 2.14-0.512-0.506-0.154 0.315-0.779 0.195 0.299-0.869 0.827-1.51 1.17-2.34zm-0.779 0.193c-0.19 0.915-0.937 2.96-1.95 3.7 0.528-1.35 1.4-2.36 1.95-3.7zm38 0c0.43 0.0243 0.0831 0.827 0.195 1.17 0.203 2.01-0.387 4.24-0.391 6.62-0.24 0.0851-0.399 0.249-0.389 0.584-0.567-2.26 0.468-4.59 0.584-6.81v-1.56zm2.14 0c1.14 2.04 1.23 5.13 1.75 7.79 0.966-2.29 0.603-4.98 1.36-7.21 0.399 2.61-0.524 5.58-0.975 7.98-0.301-0.469-0.408-0.831-0.389 0.195-0.978-1.75-0.915-4.54-1.36-6.81-0.165 2.17 0.356 4.41 0.389 7.21-0.565-1.28 0.0139-0.138-0.777 0.193 0.0648-3.42-0.308-6.71 0-9.35zm-40.6 0.148c0.0756 0.03 0.0914 0.208-0.119 0.631-0.3-0.32-0.00686-0.681 0.119-0.631zm-1.87 0.0469c0.473 0.299-0.419 0.865-0.391 1.36-0.489-0.315 0.347-0.937 0.391-1.36zm56.3 0c0.958 1.9 0.462 5.25 0.584 7.98-0.466-1.56-0.72-4.9-0.584-7.98zm-29.2 0.195c0.261 0.128 0.362 0.417 0.389 0.779-0.586 0.196-0.345-0.434-0.389-0.779zm5.45 0c0.855 1.37 1.25 4.18 1.17 5.84 0.705-0.497 0.459-1.91 0.584-2.92 0.0922-0.739 0.207-1.43 0.389-1.95 0.568 2.38-0.791 5.36-0.389 7.59-0.62-1-0.133 0.546-0.584 0.779-0.977-0.454-0.148-1.37-0.195-2.14-0.137-2.22-0.812-4.91-0.973-7.2zm3.89 0c0.529 3.03-0.0947 5.54-0.584 7.98-0.421-2.5 0.585-5.26 0.584-7.98zm0.586 0c0.0518 0.802 0.45 0.271 0.434 0.0684-0.0111-0.0131-0.0379-0.0667-0.0449-0.0684 0.0303 0.00731 0.0422 0.0341 0.0449 0.0684 0.128 0.151 0.478 0.913 0.344 0.127 0.746 1.59 1.1 3.58 1.17 5.84 0.693-0.559 0.315 2.03 0.195 2.92-0.79-2.32-0.884-5-1.36-7.98-0.709 2.29 0.473 5.69-0.193 8.18-0.33 0.682-0.502-0.381-0.391-0.779-0.551-0.0319 0.162 1.2-0.389 1.17-1.07-0.886-0.108-2.18 0-3.31 0.185-1.94 0.0807-4.14 0.195-6.23zm4.67 0c0.32 1.91 0.709 5.21 0.584 7.79-0.011 0.225 0.0668 1.62-0.195 0.779v-1.17c-0.117-0.466-0.278-0.889-0.193-1.56-0.32 0.718-0.246 1.83-0.391 2.73-0.062 0.95-0.308-0.188-0.195-0.584 0.192-3.33 0.231-5.21 0.391-7.98zm10.7 0c0.328 0.924 0.416 3.92 0.389 5.84-0.331 0.0715-0.367-0.151-0.389-0.389-0.412 0.325-0.121 1.15-0.195 1.75-0.113 0.915 0.0233 0.718-0.391 1.17-0.227-1.85 0.225-5.5 0.586-8.37zm3.11 0c0.531 0.215 0.146 1.27 0.195 1.95 0.157 2.13 0.226 3.96-0.195 6.04-0.737-2.5 0.186-5.14 0-7.98zm15.9 0.0371c0.0984 0.0567 0.249 0.423 0.301 0.547 0.943 2.17 1.77 4.45 2.34 7.01-1.39-1.92-1.56-5.06-2.72-7.2-0.0114-0.309 0.0289-0.386 0.0879-0.352zm-0.992 0.0234c0.0777-0.0047 0.165 0.325 0.125 0.523-0.0744 0.371-0.248-0.125-0.195-0.391 0.0186-0.0926 0.0444-0.131 0.0703-0.133zm-19.3 0.135c0.439 0.145 0.0752 1.09 0.193 1.56 0.435-0.0837 0.0795-0.96 0.195-1.36 0.587 1.7-0.126 5.55-0.389 7.59-0.604-0.366 0.193-1.55-0.195-1.75-0.536 0.242 0.246 1.8-0.779 1.56 0.385-2.47 0.617-5.09 0.975-7.59zm-9.93 0.193c0.315 3.2-0.183 6.07-0.779 8.96-0.48-1.91 0.625-5.97 0.779-8.96zm12.1 0.195c0.357 0.888 0.353 4.16 0.584 6.04-0.107 0.432 0.373 0.277 0.391 0.584 0.145 0.882-0.536-0.298-0.391 0.584-0.0362 0.166 0.0766 0.184 0.195 0.195-0.334 0.569-0.483-0.666-0.389-1.17-0.581 0.133 0.334 1.76-0.779 1.36 1.03-2.03-0.268-5.19 0.389-7.59zm14.2 0c0.426 0.163 0.0859 2.77 0.195 3.89-0.426-0.163-0.0866-2.77-0.195-3.89zm2.14 0c0.307 1.41 0.818 4.47 0.584 6.81-0.49-2.05-0.685-4.73-0.584-6.81zm-65.8 0.195c0.486 0.719-0.835 1.07 0 0zm58.6 0c0.461 1.7 0.122 4.27 0 6.04-0.662-2.05-0.158-3.48 0-6.04zm5.65 0c0.843-0.00953 0.422 0.921 0.584 1.56 0.452 1.77 0.994 3.94 0.973 5.06-0.0205 1.13 0.0162-0.569-0.582-0.975v-1.36c-0.43 0.0243-0.0831 0.825-0.195 1.17v1.17c-0.319-1.69-0.394-3.63-0.389-5.65-0.498-0.0851-0.114-0.185-0.391-0.973zm-63 0.121c0.146 0.0205-0.262 1.27-0.488 1.44-0.365 0.489 0.116-0.187 0-0.584-0.173 0.0865-0.347 0.17-0.389 0.389-0.525 0.34 0.346-0.653 0.195-1.17 0.324 0.492 0.303 0.726 0.584 0 0.0457-0.0538 0.0767-0.0752 0.0977-0.0723zm30.3 0.0723c0.479 2.26-0.0221 6.24-0.389 8.37-0.676-2.82 0.412-5.56 0.389-8.37zm-4.74 0.0156c-0.0307-0.0235-0.0729 0.0222-0.129 0.18-0.0257 0.256-0.029 1.41 0.195 0.779 0.0191-0.192 0.0258-0.889-0.0664-0.959zm36.5 0.18c0.292 0.754 0.144 2.35 0 3.5-0.0806 0.647-0.0137 1.64-0.584 1.75 0.335-1.61 0.268-3.63 0.584-5.26zm-46.3 0.195c0.226-0.0317 0.346 0.0415 0.389 0.193-0.128 2.2 0.079 4.87-0.193 6.81-0.0989 0.707-0.352 1.57-0.779 1.95-0.129-3.31 0.533-5.83 0.584-8.96zm-6.81 0.389c0.661 1.07-0.717 3.58 0.195 4.28-0.512 0.0558-0.182 0.0788-0.391 1.17-0.201 1.05-0.864 2.23-0.777 3.31-0.439-0.368-0.253-1.53-0.195-2.14 0.197-2.11 0.925-4.68 1.17-6.62zm-10.3 0.195c0.0548 0.509-0.0709 0.836-0.584 0.777-0.0548-0.509 0.0709-0.836 0.584-0.777zm64.6 0.434c0.0921 0.12 0.0495 0.909 0.0508 1.12-0.268 0.62-0.193-0.687-0.195-0.973 0.0671-0.155 0.114-0.19 0.145-0.15zm-21.8 0.0664c-0.0764-0.111-0.101 0.675-0.0996 0.668-0.118 0.601-0.189 2.05 0 2.92 0.0567-1.17 0.281-2.42 0.193-3.12-0.0377-0.299-0.0683-0.436-0.0938-0.473zm10.9 0.0977c0.0922 0.0705 0.0855 0.767 0.0664 0.959-0.224 0.63-0.221-0.523-0.195-0.779 0.056-0.157 0.0982-0.203 0.129-0.18zm-10.1 0.375c-0.88 0.141-0.061 0.961 0 0zm-45.8 0.322c0.171-0.00179-0.398 0.995-0.512 1.24-0.391-0.157 0.35-0.803 0.391-1.17 0.0582-0.0461 0.0966-0.0662 0.121-0.0664zm57.6 0.104c-0.0266-0.0184-0.0563 0.021-0.0879 0.158-0.171 0.741 0.105 0.975 0.195 0.584 0.0232-0.102-0.0276-0.687-0.107-0.742zm-31.7 0.193c-0.0266-0.0184-0.0563 0.0229-0.0879 0.16-0.171 0.741 0.103 0.975 0.193 0.584 0.0234-0.102-0.0257-0.689-0.105-0.744zm40.8 0.354c0.45 0.653 0.069 2.14 0.195 3.12-0.701-0.332-0.425-2.48-0.195-3.12zm-35.8 0.258c-0.0259 0.00159-0.0517 0.0402-0.0703 0.133-0.0529 0.265 0.121 0.759 0.195 0.389 0.0399-0.199-0.0473-0.526-0.125-0.521zm-5.13 0.717c0.238 0.577-0.2 1.24 0.389 1.75-0.00691-0.707 0.11-1.54-0.389-1.75zm40.2 0.0957c0.193-0.0244 0.0431 1.29 0.0898 1.66-0.346 0.519-0.141-1.14-0.193-1.56 0.0431-0.0649 0.076-0.0961 0.104-0.0996zm-56.5 0.285c0.0922 0.0214 0.133 0.608 0.0859 0.787 0.741 0.313 0.33 2.17 0.389 3.5-0.263-0.473-0.389-0.12-0.584-0.389 0.273-0.306 0.525-0.62 0.391-1.75-0.422-0.0329-0.0888 0.69-0.195 0.973-0.409 0.142-0.0983-2.1-0.195-2.92 0.0408-0.155 0.0786-0.21 0.109-0.203zm36.5 0.00781c0.05 1.12-0.147 2.48 0.193 3.31-0.245-0.0427-0.397-0.303-0.389 0.195 1.31-0.13 0.192-3.43 0.195-3.51zm11.1 0v3.31c0.722-0.341 0.518-2.93 0-3.31zm-26.3 0.195c0.403 1.87-1.12 3.65 0 4.67 0.883-0.412 0.16-4.14 0-4.67zm27.6 0.404c0.0922 0.0704 0.0854 0.767 0.0664 0.959-0.224 0.63-0.222-0.524-0.195-0.779 0.056-0.158 0.0982-0.203 0.129-0.18zm-21.6 0.225c0.0921 0.12 0.0515 0.909 0.0527 1.12-0.268 0.62-0.193-0.687-0.195-0.973 0.0671-0.155 0.112-0.19 0.143-0.15zm-12.4 0.15c-0.192 1.5-0.0842 3.29-0.584 4.48 0.55-0.02 0.825-0.732 1.36-0.193 0.0415-1.87-0.0725-2.95-0.193-4.28h-0.584zm-4.09 0.193c0.121 1.47-0.151 3.33 0.584 3.89-0.254-0.969-0.313-3.53-0.584-3.89zm40 0.0352c0.0799 0.0553 0.129 0.643 0.105 0.744-0.0906 0.391-0.365 0.157-0.193-0.584 0.0316-0.137 0.0613-0.179 0.0879-0.16zm-37.9 0.744c0.0941 0.992-1.07 2.99 0 3.12-0.0255-0.362 0.162-3.34 0-3.12zm-7.59 0.584c-0.591 0.656 0.72 0.59 0 0zm11.3 0c-0.183 0.424-0.563 2.43 0.195 2.53 0.474-0.613 0.0678-2.01-0.195-2.53zm-2.79 0.0156c0.0923 0.0705 0.0857 0.767 0.0664 0.959-0.224 0.63-0.221-0.523-0.195-0.779 0.0561-0.157 0.0981-0.203 0.129-0.18zm-15.4 0.0625c0.186-0.073 0.0589 1.16 0.0938 1.48-0.326 0.561-0.153-0.995-0.193-1.36 0.0408-0.0701 0.073-0.107 0.0996-0.117zm23 0.193c0.186-0.0729 0.0591 1.16 0.0938 1.48-0.326 0.561-0.155-0.995-0.195-1.36 0.0408-0.0701 0.075-0.107 0.102-0.117zm5.94 0.312c0.434 0.588 0.153 0.87-0.391 0.391 0.0486-0.211 0.388-0.133 0.391-0.391zm-17.4 1.01c0.0799 0.0552 0.131 0.64 0.107 0.742-0.0903 0.391-0.367 0.157-0.195-0.584 0.0316-0.137 0.0613-0.177 0.0879-0.158zm-13.3 0.221c0.0775-0.00467 0.165 0.323 0.125 0.521-0.0741 0.37-0.247-0.124-0.193-0.389 0.0185-0.0926 0.0425-0.131 0.0684-0.133z"/> + <path d="m79.9 36.4c-19.9 1.41-40.7 1.8-60.5 2.92-1.51 0.0851-2.62 0.175-4.09 0.584 4.47-0.0684 8.39-0.783 12.8-0.389-2.32 0.0481-5.06 0.252-7.4 0.389-0.777 0.0455-1.97-0.37-2.53 0.389 2.05-0.168 4.91-0.411 6.23-0.193-2.67 0.506-6.27 0.091-8.76 0.779 7.5-0.223 16.1-0.736 24.3-1.17 1.11-0.0582 2.35-0.33 3.31 0.195-8.59 0.709-19.1 0.225-26.5 1.36-0.195 0.0303-1.32-0.364-1.17 0.389 1.5-0.153 3.37-0.675 4.87-0.389 0.396 0.076 0.204 0.202 0.193 0.193 0.731 0.597 2.37 0.0448 3.31 0 16.4-0.78 34.7-1.78 51.2-2.14-9.95 0.303-20.2 1.09-30.2 1.56-8.88 0.413-19.5 0.534-27.5 1.36-0.489 0.0512-1.51-0.369-1.75 0.391 1.1-0.236 2.64-0.317 4.09-0.391 1.43-0.0727 3.07-0.374 3.89 0.195-2.66 0.0679-5.57-0.123-7.59 0.584 4.71-0.289 10.2-0.791 14.8-0.584-4.98 0.568-9.86 0.187-14.4 0.973 23-1.06 45-2.1 67.8-3.31 0.215-0.0882 0.139-0.149-0.195-0.195-1.92-0.266-4.35 0.272-6.62 0.389-14.2 0.73-28.3 1.32-43.8 1.95 16.3-1.06 33.4-1.76 49.3-2.53 0.888-0.295-1.74-0.205-2.34-0.193-14.6 0.28-30.3 1.97-45.2 2.14 15.6-1.21 31.9-1.41 48.1-2.72-2.13-0.178-4.33 0.601-6.23 0 1.88-0.0894 2.76-0.202 4.48-0.195 0.205-0.0107 0.152-0.25-0.195-0.195-11.6 0.285-22.9 1.34-33.7 1.36 11.3-0.811 23.4-0.889 34.7-1.95-13.4 0.373-25.8 1.59-38.9 1.56 10.8-0.671 23.3-0.848 35-1.75 0.75-0.0577 1.78 0.383 2.34-0.391-15.8 0.781-32.7 2.04-48.9 2.14 11.1-0.495 23.9-1.02 35.2-1.56 0.642-0.0303 5.7-0.253 1.95-0.389-3.24-0.117-6.7 0.245-10.1 0.389-10.4 0.438-20.7 1.36-31 1.36 14-0.623 30.4-1.66 45-2.34 2.19-0.102 4.53 0.158 6.62-0.584zm0.195 0.584c-1.68 0.0977-4.43 0.28-5.84 0.389-1.13 0.087-2.71-0.362-2.92 0.391 2.69-0.485 6.35-0.0107 8.76-0.779zm-2.92 1.95c-0.0684 0.646-1.12 0.305-1.75 0.389 0.347-0.653 0.891-0.1 1.75-0.389zm-45.2 0.584c-0.249 0.854-2.04 0.169-2.92 0.389-0.482 0.0245-0.331-0.239 0-0.195 1.01-0.0329 2.2 0.122 2.92-0.193zm-4.22 0.223c0.195-0.00524 0.339 0.0777 0.332 0.361-0.869 0.0396-4.9 0.00322-2.53-0.195h1.56c0.2-0.0679 0.448-0.161 0.643-0.166zm19.6 0.125c0.0961 2.66e-4 0.221 0.0129 0.377 0.041-0.374 0.52-1.05-0.0429-0.377-0.041zm-0.598 0.041c-2.45 0.735-5.11 0.454-7.79 0.584-5.46 0.264-11.8 0.69-17.5 0.973 0.288-0.744 1.26-0.373 1.75-0.389 7.78-0.254 16-0.777 23.6-1.17zm-29.4 0.154c-0.531-0.0172-2.01 0.144-1.97 0.43 0.887 0.071 0.892-0.117 1.36-0.195 0.9-0.15 0.924-0.224 0.605-0.234zm66.6 0.408c-0.246-0.0191-0.453 0.00183-0.465 0.217 0.342-0.112 1.15 0.235 1.17-0.195-0.171 0.0559-0.459-0.00239-0.705-0.0215zm-0.854 0.0215c-16.9 0.814-33.3 1.64-49.8 2.34-5.47 0.23-11 0.418-16.4 1.17 1.31 1.35 2.59 1.89 4.87 1.17-0.467-0.259-2.58 0.347-3.31-0.193 20.8-1.48 42.2-1.67 62.5-3.12 1.16-0.0825 6.24-0.251 3.7-0.391-5.06-0.279-12.2 0.6-15.8 0.779-17.1 0.867-34.5 1.61-50.8 2.34 11.4-0.948 25.3-1.37 37.6-1.95 8.37-0.389 18.7-0.712 26.5-1.36 0.356-0.0298 2.17-0.0454 0.389-0.195-3.46-0.29-6.15 0.662-8.76 0.195 3.12-0.529 6.26-0.0572 9.35-0.779zm1.15 0.367c-0.28-0.0203-0.521-9.15e-4 -0.562 0.217 0.403-0.116 1.28 0.242 1.36-0.193-0.202 0.0578-0.521-0.00318-0.801-0.0234zm-10.7 0.412c-18.8 1.35-37.8 1.88-56.1 2.92 4.47-0.677 8.87-0.789 13.2-0.973 15.4-0.65 28.1-1.36 42.8-1.95zm-54.1 0.193c-1.1 0.199-3.5 0.132-3.7 0.584 0.954-0.474 3.13 0.275 3.7-0.584zm15.4 0.00195h0.391c-0.454 0.757-1.71 0.341-2.53 0.389-2.35 0.136-6.28 0.637-8.76 0.195 3.4-0.186 7.18-0.251 10.9-0.584zm49 0.566c-0.379-0.0104-0.822-0.00661-1.27 0.0176-1.33 0.356-3.41-0.0413-4.67 0.389 1.19 0.17 4.47-0.164 6.04-0.195 1.6-0.032 1.04-0.18-0.0918-0.211zm-51.5 0.211c0.465-0.0093 0.725 0.0072 1.17 0-1.12 0.467-3.62 0.0491-1.17 0zm53.7 0.195c-22 0.986-42.4 2.08-62.5 3.12-1.63 0.0839-3.32-0.243-4.09 0.584 22.8-1.67 45.1-2.04 66.6-3.7zm-13.2 0.195c-7.79 0.371-15.8 0.698-24.9 1.17-6.32 0.324-12.7 0.682-18.9 0.973-1.57 0.0744-5.98 0.429-6.04 0.391 0.527 0.382 4.99-0.0833 7.01-0.195 13.4-0.743 28.6-1.56 40.3-2.14 1.74-0.086 5.59 0.342 7.2-0.193-0.248 0.0822-4.51-0.0076-4.67 0zm12.9 0.287c-0.248 0.017-0.562 0.1-0.781 0.102-1.14 0.0067-2.26-0.0138-2.53 0-10.5 0.531-21 0.993-31.2 1.56-9.62 0.532-20.2 0.809-29.8 1.56-0.412 0.0324-1.13-0.0174-1.36 0.389 0.452 0.322 0.909 0.0596 1.36 0 2.12-0.278 4.05-0.297 6.23-0.389 19.9-0.837 39.1-2.24 58.4-2.92 0.0428-0.264-0.139-0.314-0.387-0.297zm1.36 0.49c-10.3 0.415-20.8 1.11-31.3 1.56-8.69 0.37-18.9 0.594-27.1 1.36-0.216 0.0203-1.27-0.383-1.17 0.389 9.29-0.567 19.2-1.05 28.8-1.56 10.1-0.531 20.7-0.761 30.8-1.75zm-0.162 0.574c-0.358-9.23e-4 -0.76 0.163-1.01 0.205-2.69 0.452-5.67 0.263-8.56 0.391-8.43 0.37-16.3 0.997-24.1 1.36-9.55 0.444-19.6 0.641-28.4 1.36-1.21 0.0989-3.41-0.292-3.7 0.973 15.7-0.222 34.1-1.56 50.4-2.34 4.7-0.224 10.7-0.384 14.6-0.777 0.42-0.0424 1.41 0.433 1.56-0.391-0.0733-0.622-0.388-0.788-0.746-0.789zm-3.34 0.594c0.174 0.12 0.349 0.235 0.391 0 0.799 0.213-0.696 0.815-0.391 0zm2.14 0c-0.0598 0.264-0.488 0.162-0.779 0.195 0.0601-0.264 0.488-0.162 0.779-0.195zm-58.8 1.75c-0.784 0.143-2.45 0.127-3.31 0.193-0.481 0.0374-3.4 0.241-1.95 0.391 0.885 0.148 1.76-0.129 2.73-0.195 0.898-0.0612 2.08 0.376 2.53-0.389zm60 0.348c-1.44-0.00563-0.719 0.546 0.752 0.041-0.297-0.0283-0.547-0.0402-0.752-0.041zm-3.85 0.211c-0.575-0.0234-1.11-0.00623-1.44 0.219 0.977-0.126 2.46 0.257 3.12-0.193-0.489 0.0632-1.1-0.00204-1.68-0.0254zm-2.72 0.182c-1.32-6.1e-4 -0.521 0.546 0.699 0.0371-0.279-0.0272-0.511-0.037-0.699-0.0371zm-2.05 0.0156c-0.346-0.0217-0.652-0.00413-0.756 0.217 0.528-0.121 1.55 0.246 1.75-0.195-0.264 0.0602-0.652 1.73e-4 -0.998-0.0215zm6.95 0.178c-0.605-0.0175-1.25 0.00975-1.86 0.0391-1.06 0.0503-2.49 0.0733-3.11 0.584 2.07-0.271 4.3-0.367 6.62-0.389-0.469-0.154-1.04-0.217-1.64-0.234zm-10.1 0.0332c-3.3 0.0494-6.21 0.488-9.66 0.395 2.62 0.431 8.68-0.14 11.1-0.389-0.49-0.0116-0.97-0.0129-1.44-0.00586zm11.6 0.348c-0.181 0.019-0.406 0.202-0.428 0.242-0.184 0.342 0.718-0.0609 0.584-0.195-0.0413-0.0413-0.096-0.0532-0.156-0.0469zm-8.21 0.0469c-2.93 0.0586 0.269 0.461 1.56 0-0.498 0.0076-1.04-0.0102-1.56 0zm7.4 0c-2.36 0.431-5.39 0.188-7.79 0.584h-1.36c-0.422-0.141-1.24 0.2-0.389 0.195h1.36c1.94-0.00381 4.3-0.299 5.84-0.389 0.846-0.0496 2.73 0.329 2.34-0.391zm-24.1 0.188c-0.666 0.0157-1.27 0.0986-1.65 0.396 1.16 0.0212 1.45-0.0766 2.14-0.193 1.01 0.108 4.07-0.127 1.56-0.195-0.653 0.0287-1.39-0.0235-2.05-0.00781zm10.6 0.00781c-4.05 0.468-8.67 0.331-11.9 0.779 1.15 0.35 4.62-0.473 5.45 0.193-15.3 0.466-30.2 1.8-44.8 1.95 1.38-0.375 3.47-0.0314 4.87-0.389 1.87 0.258 2.37-0.379 2.92-0.195-1.76-0.584-6.79 0.855-7.98 0 1.36 0.0138-0.521-0.313 0.195-0.779-0.802 0.251-0.228 2.23 0.193 2.53 5.02-0.625 11.8-0.696 17.9-0.973 15.4-0.699 28.6-1.68 43.6-2.34 1.13-0.0498 2.45 0.362 3.5-0.391-5.1 0.45-11.1 0.536-16.2 0.779-1.53 0.0739-4.26 0.562-5.06 0 3.58-0.115 7.16-0.234 10.5-0.584-2.5-0.279-7.29 0.663-9.54 0 0.957 0.176 2.16-0.151 3.12-0.195 1.11-0.0517 2.45 0.367 3.11-0.389zm1.75 0c-3.3 0.0529 0.287 0.466 1.75 0-0.579 0.0074-1.14-0.0098-1.75 0zm-15.5 0.146c-0.533 0-1.07 0.0806-1.27 0.242 0.591 0.216 1.48-0.124 2.53 0-0.2-0.162-0.733-0.242-1.27-0.242zm-2.71 0.197c-1.06 0.0169-2.11 0.19-3.23 0.24-2.68 0.122-5.55-0.14-7.79 0.584 3.22-0.417 9-0.268 12.1-0.779-0.353-0.0393-0.705-0.0505-1.06-0.0449zm2.69 0.412c-0.51-0.0231-0.978-0.00747-1.24 0.217 0.848-0.126 2.2 0.255 2.72-0.193-0.424 0.0628-0.974-3.49e-4 -1.48-0.0234zm-1.44 0.0234c-0.347 0.236-1.13 0.0348-1.36 0.389 0.615 0.0312 1.18 0.00767 1.36-0.389zm-2.47 0.115c-0.77 0.00955-1.65 0.239-2.39 0.273-5.1 0.237-11.5 0.493-16 0.584-0.254 0.336-0.964 0.186-1.17 0.195-0.131 0.00596-2.35 0.195-0.584 0.195 6.63 0 13.2-0.596 18.9-0.975 0.526-0.0348 1.63-0.282 1.95-0.193-0.221-0.0612-0.466-0.0833-0.723-0.0801zm-32.8 0.0137c-0.34-0.03-0.935 0.0593-1.15 0.455 0.363-0.00143 0.436-0.161 0.779-0.195 0.799-0.081 0.715-0.23 0.375-0.26zm22.4 0.0645c-2.23-0.0922-3.94 0.347-6.23 0.195 1.07 0.484 5.6-0.0285 6.23-0.195zm43.2 0.172c-0.0514-8.94e-4 -0.115 0.00571-0.195 0.0234-0.705-0.0915-0.717 0.166-1.17 0.195-10.2 0.667-21.3 0.969-32.3 1.56-10.1 0.54-21.8 0.728-30.6 1.56-0.587 0.056-1.59-0.38-1.95 0.389 9.42-0.0427 21.9-0.763 32.5-1.36 11.1-0.626 22.7-0.69 33.5-1.95 0.261-0.00209 0.555-0.408 0.195-0.414zm-22.8 0.0234c-0.624 0.77-1.79 0.34-2.53 0.391-13.1 0.889-28.3 1.37-41.1 2.14 0.483-0.755 1.93-0.332 3.12-0.389 12.7-0.606 26.8-1.48 40.5-2.14zm-31.5 0.162c-2.41 0.0637-5.63 1.01-6.81 0.617 0.981 0.326 1.63 0.0872 3.5 0 1.55-0.0725 3.45-0.0697 4.28-0.584-0.303-0.0335-0.629-0.0423-0.973-0.0332zm2.51 0.00977c-0.542-0.0232-1.05-0.00587-1.34 0.219 0.912-0.126 2.33 0.254 2.92-0.195-0.456 0.063-1.04-2.01e-4 -1.58-0.0234zm-10.7 0.502c-1.2 0.0633-2.93 0.515-3.15 0.301 0.501 0.501 3.85-0.254 4.09-0.195-0.204-0.104-0.544-0.127-0.943-0.105zm8.54 0.105c-1.23 0.326-3.14-0.0239-4.28 0.389 1.39-0.171 3.51 0.394 4.28-0.389zm-15 0.584c-0.0303 0.229 0.033 0.553-0.195 0.584-0.00238-0.298-0.53-0.64-0.389 0 0.625-0.171 0.402 0.505 0.777 0.584-0.112-0.342 0.237-1.14-0.193-1.17zm0.664 1.27c-0.107-0.0147-0.152 0.00946-0.0801 0.0918 0.411 0.173 0.575 0.593 1.17 0.584 0.108-0.236-0.766-0.632-1.09-0.676zm21.5 1.26c0.0441 0.345-0.197 0.975 0.389 0.779-0.0267-0.363-0.128-0.651-0.389-0.779zm43.3 0.0625c-0.0259 0.00159-0.0517 0.0402-0.0703 0.133-0.0527 0.265 0.121 0.759 0.195 0.389 0.04-0.199-0.0473-0.526-0.125-0.521z" fill="#cbc1b6"/> +</svg> diff --git a/scour/scour.py b/scour/scour.py index 9d19906..8c907de 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -2918,6 +2918,7 @@ def scourCoordinates(data, options, force_whitespace=False, control_points=[], f for coord in data: is_control_point = c in control_points scouredCoord = scourUnitlessLength(coord, + nonsci_output=options.nonsci_output, renderer_workaround=options.renderer_workaround, is_control_point=is_control_point) # don't output a space if this number starts with a dot (.) or minus sign (-); we only need a space if @@ -2964,7 +2965,8 @@ def scourLength(length): return scourUnitlessLength(length.value) + Unit.str(length.units) -def scourUnitlessLength(length, renderer_workaround=False, is_control_point=False): # length is of a numeric type +def scourUnitlessLength(length, nonsci_output=False, renderer_workaround=False, is_control_point=False): # length is of a numeric type + """ Scours the numeric part of a length only. Does not accept units. @@ -3001,6 +3003,12 @@ def scourUnitlessLength(length, renderer_workaround=False, is_control_point=Fals nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot return_value = nonsci + + # Prevent scientific notation, interferes with some label maker software + if nonsci_output: + return return_value + + # 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: @@ -3958,6 +3966,9 @@ _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") +_option_group_optimization.add_option("--nonsci-output", + action="store_true", dest="nonsci_output", default=False, + help="Remove scientific notation from path data") _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")