| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2014 Google Inc. All rights reserved. | |
| 3 # | |
| 4 # Redistribution and use in source and binary forms, with or without | |
| 5 # modification, are permitted provided that the following conditions are | |
| 6 # met: | |
| 7 # | |
| 8 # * Redistributions of source code must retain the above copyright | |
| 9 # notice, this list of conditions and the following disclaimer. | |
| 10 # * Redistributions in binary form must reproduce the above | |
| 11 # copyright notice, this list of conditions and the following disclaimer | |
| 12 # in the documentation and/or other materials provided with the | |
| 13 # distribution. | |
| 14 # * Neither the name of Google Inc. nor the names of its | |
| 15 # contributors may be used to endorse or promote products derived from | |
| 16 # this software without specific prior written permission. | |
| 17 # | |
| 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 29 | |
| 30 import re | |
| 31 import string | |
| 32 import sys | |
| 33 import xml.dom.minidom | |
| 34 | |
| 35 | |
| 36 def _optimize_number(value): | |
| 37 try: | |
| 38 if value[0] == "#" or value[0] == "n": | |
| 39 return value | |
| 40 numeric = round(float(value), 2) | |
| 41 short = int(numeric) | |
| 42 if short == numeric: | |
| 43 return str(short) | |
| 44 return str(numeric) | |
| 45 except: | |
| 46 return value | |
| 47 | |
| 48 | |
| 49 def _optimize_value(value, default): | |
| 50 value = value.strip() | |
| 51 if value.endswith("px"): | |
| 52 value = value[:-2] | |
| 53 if value.endswith("pt"): | |
| 54 print "WARNING: 'pt' size units are undesirable." | |
| 55 if len(value) == 7 and value[0] == "#" and value[1] == value[2] and value[3]
== value[4] and value[6] == value[6]: | |
| 56 value = "#" + value[1] + value[3] + value[5] | |
| 57 value = _optimize_number(value) | |
| 58 if value == default: | |
| 59 value = "" | |
| 60 return value | |
| 61 | |
| 62 | |
| 63 def _optimize_values(node, defaults): | |
| 64 items = {} | |
| 65 if node.hasAttribute("style"): | |
| 66 for item in node.getAttribute("style").strip(";").split(";"): | |
| 67 [key, value] = item.split(":", 1) | |
| 68 key = key.strip() | |
| 69 if key not in defaults: | |
| 70 continue | |
| 71 items[key] = _optimize_value(value, defaults[key]) | |
| 72 | |
| 73 for key in defaults.keys(): | |
| 74 if node.hasAttribute(key): | |
| 75 value = _optimize_value(node.getAttribute(key), defaults[key]) | |
| 76 items[key] = value | |
| 77 | |
| 78 if len([(key, value) for key, value in items.iteritems() if value != ""]) >
4: | |
| 79 style = [] | |
| 80 for key, value in items.iteritems(): | |
| 81 if node.hasAttribute(key): | |
| 82 node.removeAttribute(key) | |
| 83 if value != "": | |
| 84 style.append(key + ":" + value) | |
| 85 node.setAttribute("style", string.join(sorted(style), ";")) | |
| 86 else: | |
| 87 if node.hasAttribute("style"): | |
| 88 node.removeAttribute("style") | |
| 89 for key, value in items.iteritems(): | |
| 90 if value == "": | |
| 91 if node.hasAttribute(key): | |
| 92 node.removeAttribute(key) | |
| 93 else: | |
| 94 node.setAttribute(key, value) | |
| 95 | |
| 96 | |
| 97 def _optimize_path(value): | |
| 98 path = [] | |
| 99 commands = "mMzZlLhHvVcCsSqQtTaA" | |
| 100 last = 0 | |
| 101 raw = " " + value + " " | |
| 102 for i in range(len(raw)): | |
| 103 if raw[i] in [" ", ","]: | |
| 104 if last < i: | |
| 105 path.append(raw[last:i]) | |
| 106 # Consumed whitespace | |
| 107 last = i + 1 | |
| 108 elif raw[i] == "-" and raw[i - 1] != "e" and raw[i - 1] != "e": | |
| 109 if last < i: | |
| 110 path.append(raw[last:i]) | |
| 111 last = i | |
| 112 elif raw[i] in commands: | |
| 113 if last < i: | |
| 114 path.append(raw[last:i]) | |
| 115 path.append(raw[i]) | |
| 116 # Consumed command | |
| 117 last = i + 1 | |
| 118 out = [] | |
| 119 need_space = False | |
| 120 for item in path: | |
| 121 if item in commands: | |
| 122 need_space = False | |
| 123 else: | |
| 124 item = _optimize_number(item) | |
| 125 if need_space and item[0] != "-": | |
| 126 out.append(" ") | |
| 127 need_space = True | |
| 128 out.append(item) | |
| 129 return string.join(out, "") | |
| 130 | |
| 131 | |
| 132 def _optimize_paths(dom): | |
| 133 for node in dom.getElementsByTagName("path"): | |
| 134 path = node.getAttribute("d") | |
| 135 node.setAttribute("d", _optimize_path(path)) | |
| 136 | |
| 137 | |
| 138 def _check_groups(dom, errors): | |
| 139 if len(dom.getElementsByTagName("g")) != 0: | |
| 140 errors.append("Groups are prohibited.") | |
| 141 | |
| 142 | |
| 143 def _check_text(dom, errors): | |
| 144 if len(dom.getElementsByTagName("text")) != 0: | |
| 145 errors.append("Text elements prohibited.") | |
| 146 | |
| 147 | |
| 148 def _check_transform(dom, errors): | |
| 149 if (any(path.hasAttribute("transform") for path in dom.getElementsByTagName(
"path")) or | |
| 150 any(rect.hasAttribute("transform") for rect in dom.getElementsByTagName(
"rect"))): | |
| 151 errors.append("Transforms are prohibited.") | |
| 152 | |
| 153 | |
| 154 def _cleanup_dom_recursively(node, dtd): | |
| 155 junk = [] | |
| 156 for child in node.childNodes: | |
| 157 if child.nodeName in dtd: | |
| 158 _cleanup_dom_recursively(child, dtd[child.nodeName]) | |
| 159 else: | |
| 160 junk.append(child) | |
| 161 | |
| 162 for child in junk: | |
| 163 node.removeChild(child) | |
| 164 | |
| 165 | |
| 166 def _cleanup_dom(dom): | |
| 167 dtd = { | |
| 168 "svg": { | |
| 169 "sodipodi:namedview": { | |
| 170 "inkscape:grid": {}}, | |
| 171 "defs": { | |
| 172 "linearGradient": { | |
| 173 "stop": {}}, | |
| 174 "radialGradient": { | |
| 175 "stop": {}}}, | |
| 176 "path": {}, | |
| 177 "rect": {}}} | |
| 178 _cleanup_dom_recursively(dom, dtd) | |
| 179 | |
| 180 | |
| 181 def _cleanup_sodipodi(dom): | |
| 182 for node in dom.getElementsByTagName("svg"): | |
| 183 for key in node.attributes.keys(): | |
| 184 if key not in ["height", "version", "width", "xml:space", "xmlns", "
xmlns:xlink", "xmlns:sodipodi", "xmlns:inkscape"]: | |
| 185 node.removeAttribute(key) | |
| 186 | |
| 187 for node in dom.getElementsByTagName("sodipodi:namedview"): | |
| 188 for key in node.attributes.keys(): | |
| 189 if key != "showgrid": | |
| 190 node.removeAttribute(key) | |
| 191 | |
| 192 for nodeName in ["defs", "linearGradient", "path", "radialGradient", "rect",
"stop", "svg"]: | |
| 193 for node in dom.getElementsByTagName(nodeName): | |
| 194 for key in node.attributes.keys(): | |
| 195 if key.startswith("sodipodi:") or key.startswith("inkscape:"): | |
| 196 node.removeAttribute(key) | |
| 197 | |
| 198 | |
| 199 def _cleanup_ids(dom): | |
| 200 for nodeName in ["defs", "path", "rect", "sodipodi:namedview", "stop", "svg"
]: | |
| 201 for node in dom.getElementsByTagName(nodeName): | |
| 202 if node.hasAttribute("id"): | |
| 203 node.removeAttribute("id") | |
| 204 | |
| 205 | |
| 206 def _optimize_path_attributes(dom): | |
| 207 defaults = { | |
| 208 "fill": "#000", | |
| 209 "fill-opacity": "1", | |
| 210 "fill-rule": "nonzero", | |
| 211 "opacity": "1", | |
| 212 "stroke": "none", | |
| 213 "stroke-dasharray": "none", | |
| 214 "stroke-linecap": "butt", | |
| 215 "stroke-linejoin": "miter", | |
| 216 "stroke-miterlimit": "4", | |
| 217 "stroke-opacity": "1", | |
| 218 "stroke-width": "1"} | |
| 219 for nodeName in ["path", "rect"]: | |
| 220 for node in dom.getElementsByTagName(nodeName): | |
| 221 _optimize_values(node, defaults) | |
| 222 | |
| 223 | |
| 224 def _optimize_stop_attributes(dom): | |
| 225 defaults = { | |
| 226 "stop-color": "#000", | |
| 227 "stop-opacity": "1"} | |
| 228 for node in dom.getElementsByTagName("stop"): | |
| 229 _optimize_values(node, defaults) | |
| 230 | |
| 231 | |
| 232 def _cleanup_gradients(dom): | |
| 233 while True: | |
| 234 gradients = [] | |
| 235 for nodeName in ["linearGradient", "radialGradient"]: | |
| 236 for node in dom.getElementsByTagName(nodeName): | |
| 237 name = node.getAttribute("id") | |
| 238 gradients.append({"node": node, "ref": "#" + name, "url": "url(#
" + name + ")", "has_ref": False}) | |
| 239 for nodeName in ["linearGradient", "path", "radialGradient", "rect"]: | |
| 240 for node in dom.getElementsByTagName(nodeName): | |
| 241 for key in node.attributes.keys(): | |
| 242 if key == "id": | |
| 243 continue | |
| 244 value = node.getAttribute(key) | |
| 245 for gradient in gradients: | |
| 246 if gradient["has_ref"] == False: | |
| 247 if value == gradient["ref"] or value.find(gradient["
url"]) != -1: | |
| 248 gradient["has_ref"] = True | |
| 249 finished = True | |
| 250 for gradient in gradients: | |
| 251 if gradient["has_ref"] == False: | |
| 252 gradient["node"].parentNode.removeChild(gradient["node"]) | |
| 253 finished = False | |
| 254 if finished: | |
| 255 break | |
| 256 | |
| 257 | |
| 258 def _generate_name(num): | |
| 259 letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
| 260 n = len(letters) | |
| 261 if num < n: | |
| 262 return letters[num] | |
| 263 return letters[num / n] + letters[num % n] | |
| 264 | |
| 265 | |
| 266 def _optimize_gradient_ids(dom): | |
| 267 gradients = [] | |
| 268 names = {} | |
| 269 for nodeName in ["linearGradient", "radialGradient"]: | |
| 270 for node in dom.getElementsByTagName(nodeName): | |
| 271 name = node.getAttribute("id") | |
| 272 gradients.append({"node": node, "name": name, "ref": "#" + name, "ur
l": "url(#" + name + ")", "new_name": None}) | |
| 273 names[name] = True | |
| 274 cntr = 0 | |
| 275 for gradient in gradients: | |
| 276 if len(gradient["name"]) > 2: | |
| 277 while True: | |
| 278 new_name = _generate_name(cntr) | |
| 279 cntr = cntr + 1 | |
| 280 if new_name not in names: | |
| 281 gradient["new_name"] = new_name | |
| 282 gradient["node"].setAttribute("id", new_name) | |
| 283 break | |
| 284 if cntr == 0: | |
| 285 return | |
| 286 gradients = [gradient for gradient in gradients if gradient["new_name"] is n
ot None] | |
| 287 for nodeName in ["linearGradient", "path", "radialGradient", "rect"]: | |
| 288 for node in dom.getElementsByTagName(nodeName): | |
| 289 for key in node.attributes.keys(): | |
| 290 if key == "id": | |
| 291 continue | |
| 292 value = node.getAttribute(key) | |
| 293 for gradient in gradients: | |
| 294 if value == gradient["ref"]: | |
| 295 node.setAttribute(key, "#" + gradient["new_name"]) | |
| 296 elif value.find(gradient["url"]) != -1: | |
| 297 value = value.replace(gradient["url"], "url(#" + gradien
t["new_name"] + ")") | |
| 298 node.setAttribute(key, value) | |
| 299 | |
| 300 | |
| 301 def _build_xml(dom): | |
| 302 raw_xml = dom.toxml("utf-8") | |
| 303 # Turn to one-node-per-line | |
| 304 pretty_xml = re.sub("([^?])(/?>)(?!</)", "\\1\\n\\2", raw_xml) | |
| 305 return pretty_xml | |
| 306 | |
| 307 | |
| 308 def optimize_svg(file, errors): | |
| 309 try: | |
| 310 dom = xml.dom.minidom.parse(file) | |
| 311 except: | |
| 312 errors.append("Can't parse XML.") | |
| 313 return | |
| 314 | |
| 315 _check_groups(dom, errors) | |
| 316 _check_text(dom, errors) | |
| 317 _check_transform(dom, errors) | |
| 318 if len(errors) != 0: | |
| 319 return | |
| 320 | |
| 321 _cleanup_dom(dom) | |
| 322 _cleanup_ids(dom) | |
| 323 _cleanup_sodipodi(dom) | |
| 324 _cleanup_gradients(dom) | |
| 325 | |
| 326 _optimize_gradient_ids(dom) | |
| 327 _optimize_path_attributes(dom) | |
| 328 _optimize_stop_attributes(dom) | |
| 329 _optimize_paths(dom) | |
| 330 # TODO: Bake nested gradients | |
| 331 # TODO: Optimize gradientTransform | |
| 332 | |
| 333 with open(file, "w") as text_file: | |
| 334 text_file.write(_build_xml(dom)) | |
| 335 | |
| 336 | |
| 337 if __name__ == '__main__': | |
| 338 if len(sys.argv) != 1: | |
| 339 print('usage: %s input_file' % sys.argv[0]) | |
| 340 sys.exit(1) | |
| 341 errors = [] | |
| 342 optimize_svg(sys.argv[1], errors) | |
| 343 for error in errors: | |
| 344 print "ERROR: %s" % (error) | |
| 345 if len(errors) != 0: | |
| 346 sys.exit(1) | |
| 347 else: | |
| 348 sys.exit(0) | |
| OLD | NEW |