[render] Fix name of 16x16 icons
[moblin-icon-theme.git] / render.py
blobf5dfd5cbd8cab18dc08a9b1dbe7358e66ff3b495
1 #!/usr/bin/python
2 # coding=utf-8
3 """render-application-icons
5 A small script to generate Moblin icons for applications.
7 Usage: render-applications-icons [options] filename
9 Options:
10 -o, --output directory where to put the generated files
11 -i, --id XML id only generate the corresponding icon
12 -h, --help print this help
13 -v, --verbose verbose output"""
15 import getopt
16 import libxml2
17 import os
18 import re
19 import string
20 import shutil
21 import sys
22 import tempfile
24 __author__ = 'Damien Lespiau <damien.lespiau@intel.com'
25 __version__ = '0.1'
26 __date__ = '20100107'
27 __copyright__ = 'Copyright (©) 2009-10 Intel Corporation'
28 __license__ = 'GPL v2'
30 verbose = 0
31 opt_xml_id = None
33 class RendererException(Exception):
34 pass
36 class FatalError(RendererException):
37 '''An error not related to the user input'''
39 class ParseError(RendererException):
40 '''Could not parse the icon theme SVG file'''
42 def note(message):
43 if verbose:
44 print message
46 def debug(message):
47 if verbose > 1:
48 print message
50 class ImageMagick:
51 def __init___(self):
52 pass
54 def composite(self, file1, fil2, output):
55 cmd = "composite -gravity center %s %s %s" % (file1, fil2, output)
56 os.system(cmd)
58 class Inkscape:
59 def __init__(self, filename):
60 self.binary = 'inkscape'
61 self.svg = filename
63 def export(self, id, output, witdh, height):
64 global verbose
66 redirect = ""
67 if verbose < 2:
68 redirect = "1> /dev/null 2> /dev/null"
70 cmd = "%s -i %s -e %s -w %d -h %d %s %s" % (self.binary,
71 id,
72 output,
73 witdh,
74 height,
75 self.svg, redirect)
76 os.system(cmd)
78 class SVGFile:
79 def __init__(self, filename):
80 self.filename = filename
81 self.doc = libxml2.parseFile(filename)
82 if self.doc.name != filename:
83 raise ParseError("Error parsing %s" % filename)
84 self.ctx = self.doc.xpathNewContext()
86 self.ctx.xpathRegisterNs('svg', 'http://www.w3.org/2000/svg')
87 self.ctx.xpathRegisterNs('sodipodi',
88 'http://sodipodi.sourceforge.net/DTD/'
89 'sodipodi-0.dtd')
90 self.ctx.xpathRegisterNs('inkscape',
91 'http://www.inkscape.org/namespaces/inkscape')
93 def xpath_eval(self, xpath):
94 return self.ctx.xpathEval(xpath)
96 def change_stroke(self, color):
97 res = self.xpath_eval("/svg:svg"
98 "/svg:g[@inkscape:groupmode='layer' and "
99 " @inkscape:label='Artwork']"
100 "/svg:g[@inkscape:groupmode='layer' and "
101 " @inkscape:label='Applications']"
102 "/descendant::*")
104 for node in res:
105 style = node.prop('style')
106 if not style:
107 continue
108 style = re.sub(r'stroke:.*?;', 'stroke:%s;' % color, style)
109 node.setProp('style', style)
111 def change_stroke_width(self, width):
112 res = self.xpath_eval("/svg:svg"
113 "/svg:g[@inkscape:groupmode='layer' and "
114 " @inkscape:label='Artwork']"
115 "/svg:g[@inkscape:groupmode='layer' and "
116 " @inkscape:label='Applications']"
117 "/descendant::*")
118 re_stroke_width = re.compile(r'stroke-width:(.*?);')
120 for node in res:
121 style = node.prop('style')
122 if not style:
123 continue
124 found_stroke_width = re_stroke_width.search(style)
125 if not found_stroke_width:
126 continue
127 old_width = float(found_stroke_width.group(1))
128 # we assume that the original number represents 2px (whatever the
129 # transform matrix used. If that assumption changes, this code has
130 # to change to take the transform matrix into account
131 new_width = width * old_width / 2
132 style = re.sub('stroke-width:.*?;',
133 "stroke-width:%0.8f;" % new_width,
134 style)
135 node.setProp('style', style)
137 def write(self):
138 self.doc.saveFile(self.filename)
140 class IconTheme:
141 def __init__(self, filename, output_dir='.'):
142 self.file = SVGFile(filename)
143 self.set_output_directory(output_dir)
145 def set_output_directory(self, directory):
146 self.output_dir = directory
147 if not os.path.exists(directory):
148 os.makedirs(directory)
152 def generate_app_svg(self, filename):
153 doc = libxml2.newDoc("1.0")
155 # add the root element
156 res = self.file.xpath_eval("/svg:svg")
157 svg = res[0].copyNode(2)
158 doc.setRootElement(svg)
160 # rectangle layer
161 res = self.file.xpath_eval("/svg:svg"
162 "/svg:g[@inkscape:groupmode='layer' and "
163 " @id='Rectangles']")
164 rectangles_layer = res[0].copyNode(2)
165 svg.addChild(rectangles_layer)
167 # rectangles
168 res = self.file.xpath_eval("/svg:svg"
169 "/svg:g[@inkscape:groupmode='layer' and "
170 " @id='Rectangles']"
171 "/svg:rect[starts-with(@id,'moblin-')]")
172 for rect in res:
173 rectangle = rect.copyNode(2)
174 rectangles_layer.addChild(rectangle)
176 # Artwork layer
177 res = self.file.xpath_eval("/svg:svg"
178 "/svg:g[@inkscape:groupmode='layer' and "
179 " @inkscape:label='Artwork']")
180 artwork_layer = res[0].copyNode(2)
181 svg.addChild(artwork_layer)
183 # Applications layer (and its whole subtree)
184 res = self.file.xpath_eval("/svg:svg"
185 "/svg:g[@inkscape:groupmode='layer' and "
186 " @inkscape:label='Artwork']"
187 "/svg:g[@inkscape:groupmode='layer' and "
188 " @inkscape:label='Applications']")
189 applications_layer = res[0].copyNode(1)
190 artwork_layer.addChild(applications_layer)
192 doc.saveFile(filename)
193 return SVGFile (filename)
195 def create_output_dir(self, size, name):
196 output_dir = os.path.join(self.output_dir,
197 "%dx%d" % (size, size),
198 name)
199 if not os.path.exists(output_dir):
200 os.makedirs(output_dir)
202 return output_dir
204 def generate_icons(self):
205 res = self.file.xpath_eval("/svg:svg"
206 "/svg:g[@inkscape:groupmode='layer' and "
207 " @id='Rectangles']"
208 "/svg:rect")
209 re_default_rect_id = re.compile(r'rect[0-9]+')
210 inkscape = Inkscape(self.file.filename)
212 output_dir_16 = self.create_output_dir(16, 'icons')
213 output_dir_24 = self.create_output_dir(24, 'icons')
214 output_dir_48 = self.create_output_dir(48, 'icons')
215 dirs = { '16': output_dir_16, '24': output_dir_24 }
217 for rect in res:
218 global opt_xml_id
220 id = rect.prop('id')
221 width = rect.prop('width')
222 height = rect.prop('height')
224 # if opt_xml_id, only generate this id
225 if opt_xml_id and id != opt_xml_id:
226 continue
228 if re_default_rect_id.match(id):
229 debug("Dropping " + id)
230 continue
232 if not((width == '16' and height == '16') or
233 (width == '24' and height == '24')):
234 debug("Dropping " + id)
235 continue
237 # strip the moblin- prefix
238 if id.startswith('moblin-'):
239 id = id[7:]
241 # strip the 16- prefix
242 if width == '16' and id.startswith('16-'):
243 id = id[3:]
245 file = os.path.join(dirs[width], id + '.png')
246 print('Generating ' + file)
247 inkscape.export(id, file, int(width), int(height))
249 # generate 48x48 icons with the 24x24 rects
250 if width == '24':
251 file_48 = os.path.join (output_dir_48, id + '.png')
252 print('Generating ' + file_48)
253 inkscape.export(id, file_48, 48, 48)
255 def generate_app_icons(self, tile_size, fg_size):
256 # that a way to say: "Don't try with any other size"
257 if tile_size != 32 and tile_size != 48:
258 return
260 # create the output directory if necessary
261 output_dir = os.path.join(self.output_dir,
262 "%dx%d" % (tile_size, tile_size),
263 'apps')
264 if not os.path.exists(output_dir):
265 os.makedirs(output_dir)
267 # temporary directory to store the foreground of the icons
268 tmp_dir = tempfile.mkdtemp(dir='.')
269 # let's invoke the power of image magick
270 magick = ImageMagick()
272 # generate a svg with the app icons to manipulate the xml
273 app_svg_filename = os.path.join(tmp_dir, "apps-%d.svg" % tile_size)
274 app_svg = self.generate_app_svg(app_svg_filename)
275 app_svg.change_stroke('white')
276 if tile_size == 48:
277 app_svg.change_stroke_width(1.7)
278 app_svg.write()
280 inkscape = Inkscape(app_svg_filename)
282 # look for the applications' rectangles
283 res = app_svg.xpath_eval("/svg:svg"
284 "/svg:g[@inkscape:groupmode='layer' and "
285 " @id='Rectangles']"
286 "/svg:rect")
288 for node in res:
289 global opt_xml_id
291 fg_name = node.prop('id')
292 fg_file = os.path.join(tmp_dir, fg_name + '.png')
294 # if opt_xml_id, only generate this id
295 if opt_xml_id and fg_name != opt_xml_id:
296 continue
298 tile_name = node.prop('label')
299 tile_file = os.path.join('tiles',
300 "%dx%d" % (tile_size, tile_size),
301 tile_name + '.png')
303 icon_file = os.path.join (output_dir, fg_name + '.png')
305 print('Generating ' + icon_file)
307 inkscape.export(fg_name, fg_file, fg_size, fg_size)
308 magick.composite(fg_file, tile_file, icon_file)
310 # remove the temporary directory
311 shutil.rmtree(tmp_dir)
313 def usage():
314 print(__doc__)
315 sys.exit(1)
317 def main(argv):
318 opt_output = "."
320 try:
321 opts, args = getopt.getopt(argv, 'hvo:i:', ('help',
322 'verbose',
323 'output=',
324 'id='))
325 except getopt.GetoptError:
326 usage()
327 for opt, arg, in opts:
328 global verbose
329 if opt in ('-h', '--help'):
330 usage()
331 elif opt in ('-v', '--verbose'):
332 verbose = 1
333 elif opt in ('--extra-verbose'):
334 verbose = 2
335 elif opt in ('-o', '--output'):
336 opt_output = arg
337 elif opt in ('-i', '--id'):
338 global opt_xml_id
339 opt_xml_id = arg
340 else:
341 assert False, "Unhandled option"
343 if len(args) != 1:
344 usage()
346 xml_file = args[0]
347 note("Using %s" % xml_file)
348 icon_theme = IconTheme(xml_file)
349 icon_theme.set_output_directory(opt_output)
350 icon_theme.generate_icons()
351 icon_theme.generate_app_icons(32, 24)
352 icon_theme.generate_app_icons(48, 36)
354 if __name__ == '__main__':
355 main(sys.argv[1:])