2 # Copyright (c) 2014 Google Inc. All rights reserved.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
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
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.
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.
33 import xml.dom.minidom
36 def _optimize_number(value):
38 if value[0] == "#" or value[0] == "n":
40 numeric = round(float(value), 2)
49 def _optimize_value(value, default):
51 if value.endswith("px"):
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)
63 def _optimize_values(node, defaults):
65 if node.hasAttribute("style"):
66 for item in node.getAttribute("style").strip(";").split(";"):
67 [key, value] = item.split(":", 1)
69 if key not in defaults:
71 items[key] = _optimize_value(value, defaults[key])
73 for key in defaults.keys():
74 if node.hasAttribute(key):
75 value = _optimize_value(node.getAttribute(key), defaults[key])
78 if len([(key, value) for key, value in items.iteritems() if value != ""]) > 4:
80 for key, value in items.iteritems():
81 if node.hasAttribute(key):
82 node.removeAttribute(key)
84 style.append(key + ":" + value)
85 node.setAttribute("style", string.join(sorted(style), ";"))
87 if node.hasAttribute("style"):
88 node.removeAttribute("style")
89 for key, value in items.iteritems():
91 if node.hasAttribute(key):
92 node.removeAttribute(key)
94 node.setAttribute(key, value)
97 def _optimize_path(value):
99 commands = "mMzZlLhHvVcCsSqQtTaA"
101 raw = " " + value + " "
102 for i in range(len(raw)):
103 if raw[i] in [" ", ","]:
105 path.append(raw[last:i])
106 # Consumed whitespace
108 elif raw[i] == "-" and raw[i - 1] != "e" and raw[i - 1] != "e":
110 path.append(raw[last:i])
112 elif raw[i] in commands:
114 path.append(raw[last:i])
124 item = _optimize_number(item)
125 if need_space and item[0] != "-":
129 return string.join(out, "")
132 def _optimize_paths(dom):
133 for node in dom.getElementsByTagName("path"):
134 path = node.getAttribute("d")
135 node.setAttribute("d", _optimize_path(path))
138 def _check_groups(dom, errors):
139 if len(dom.getElementsByTagName("g")) != 0:
140 errors.append("Groups are prohibited.")
143 def _check_text(dom, errors):
144 if len(dom.getElementsByTagName("text")) != 0:
145 errors.append("Text elements prohibited.")
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.")
154 def _cleanup_dom_recursively(node, dtd):
156 for child in node.childNodes:
157 if child.nodeName in dtd:
158 _cleanup_dom_recursively(child, dtd[child.nodeName])
163 node.removeChild(child)
166 def _cleanup_dom(dom):
169 "sodipodi:namedview": {
170 "inkscape:grid": {}},
178 _cleanup_dom_recursively(dom, dtd)
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)
187 for node in dom.getElementsByTagName("sodipodi:namedview"):
188 for key in node.attributes.keys():
189 if key != "showgrid":
190 node.removeAttribute(key)
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)
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")
206 def _optimize_path_attributes(dom):
210 "fill-rule": "nonzero",
213 "stroke-dasharray": "none",
214 "stroke-linecap": "butt",
215 "stroke-linejoin": "miter",
216 "stroke-miterlimit": "4",
217 "stroke-opacity": "1",
219 for nodeName in ["path", "rect"]:
220 for node in dom.getElementsByTagName(nodeName):
221 _optimize_values(node, defaults)
224 def _optimize_stop_attributes(dom):
226 "stop-color": "#000",
228 for node in dom.getElementsByTagName("stop"):
229 _optimize_values(node, defaults)
232 def _cleanup_gradients(dom):
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():
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
250 for gradient in gradients:
251 if gradient["has_ref"] == False:
252 gradient["node"].parentNode.removeChild(gradient["node"])
258 def _generate_name(num):
259 letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
263 return letters[num / n] + letters[num % n]
266 def _optimize_gradient_ids(dom):
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, "url": "url(#" + name + ")", "new_name": None})
275 for gradient in gradients:
276 if len(gradient["name"]) > 2:
278 new_name = _generate_name(cntr)
280 if new_name not in names:
281 gradient["new_name"] = new_name
282 gradient["node"].setAttribute("id", new_name)
286 gradients = [gradient for gradient in gradients if gradient["new_name"] is not None]
287 for nodeName in ["linearGradient", "path", "radialGradient", "rect"]:
288 for node in dom.getElementsByTagName(nodeName):
289 for key in node.attributes.keys():
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(#" + gradient["new_name"] + ")")
298 node.setAttribute(key, value)
302 raw_xml = dom.toxml("utf-8")
303 # Turn to one-node-per-line
304 pretty_xml = re.sub("([^?])(/?>)(?!</)", "\\1\\n\\2", raw_xml)
308 def optimize_svg(file, errors):
310 dom = xml.dom.minidom.parse(file)
312 errors.append("Can't parse XML.")
315 _check_groups(dom, errors)
316 _check_text(dom, errors)
317 _check_transform(dom, errors)
323 _cleanup_sodipodi(dom)
324 _cleanup_gradients(dom)
326 _optimize_gradient_ids(dom)
327 _optimize_path_attributes(dom)
328 _optimize_stop_attributes(dom)
330 # TODO: Bake nested gradients
331 # TODO: Optimize gradientTransform
333 with open(file, "w") as text_file:
334 text_file.write(_build_xml(dom))
337 if __name__ == '__main__':
338 if len(sys.argv) != 1:
339 print('usage: %s input_file' % sys.argv[0])
342 optimize_svg(sys.argv[1], errors)
344 print "ERROR: %s" % (error)