Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / scripts / optimize_svg_file.py
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, "url": "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 not 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(#" + gradient["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)