3 """render-application-icons
5 A small script to generate Moblin icons for applications.
7 Usage: render-applications-icons [options] filename
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"""
24 __author__
= 'Damien Lespiau <damien.lespiau@intel.com'
27 __copyright__
= 'Copyright (©) 2009-10 Intel Corporation'
28 __license__
= 'GPL v2'
33 class RendererException(Exception):
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'''
54 def composite(self
, file1
, fil2
, output
):
55 cmd
= "composite -gravity center %s %s %s" % (file1
, fil2
, output
)
59 def __init__(self
, filename
):
60 self
.binary
= 'inkscape'
63 def export(self
, id, output
, witdh
, height
):
68 redirect
= "1> /dev/null 2> /dev/null"
70 cmd
= "%s -i %s -e %s -w %d -h %d %s %s" % (self
.binary
,
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/'
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']"
105 style
= node
.prop('style')
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']"
118 re_stroke_width
= re
.compile(r
'stroke-width:(.*?);')
121 style
= node
.prop('style')
124 found_stroke_width
= re_stroke_width
.search(style
)
125 if not found_stroke_width
:
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
,
135 node
.setProp('style', style
)
138 self
.doc
.saveFile(self
.filename
)
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
)
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
)
168 res
= self
.file.xpath_eval("/svg:svg"
169 "/svg:g[@inkscape:groupmode='layer' and "
171 "/svg:rect[starts-with(@id,'moblin-')]")
173 rectangle
= rect
.copyNode(2)
174 rectangles_layer
.addChild(rectangle
)
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
),
199 if not os
.path
.exists(output_dir
):
200 os
.makedirs(output_dir
)
204 def generate_icons(self
):
205 res
= self
.file.xpath_eval("/svg:svg"
206 "/svg:g[@inkscape:groupmode='layer' and "
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
}
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
:
228 if re_default_rect_id
.match(id):
229 debug("Dropping " + id)
232 if not((width
== '16' and height
== '16') or
233 (width
== '24' and height
== '24')):
234 debug("Dropping " + id)
237 # strip the moblin- prefix
238 if id.startswith('moblin-'):
241 # strip the 16- prefix
242 if width
== '16' and id.startswith('16-'):
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
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:
260 # create the output directory if necessary
261 output_dir
= os
.path
.join(self
.output_dir
,
262 "%dx%d" % (tile_size
, tile_size
),
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')
277 app_svg
.change_stroke_width(1.7)
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 "
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
:
298 tile_name
= node
.prop('label')
299 tile_file
= os
.path
.join('tiles',
300 "%dx%d" % (tile_size
, tile_size
),
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
)
321 opts
, args
= getopt
.getopt(argv
, 'hvo:i:', ('help',
325 except getopt
.GetoptError
:
327 for opt
, arg
, in opts
:
329 if opt
in ('-h', '--help'):
331 elif opt
in ('-v', '--verbose'):
333 elif opt
in ('--extra-verbose'):
335 elif opt
in ('-o', '--output'):
337 elif opt
in ('-i', '--id'):
341 assert False, "Unhandled option"
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__':