1 #!/usr/bin/env python
2 '''
3 Copyright (C) 2010 Aurelio A. Heckert, aurium (a) gmail dot com
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 '''
20 from webslicer_effect import *
21 import inkex
22 import gettext
23 import os.path
24 import subprocess
25 import tempfile
26 import commands
28 _ = gettext.gettext
30 class WebSlicer_Export(WebSlicer_Effect):
32 def __init__(self):
33 WebSlicer_Effect.__init__(self)
34 self.OptionParser.add_option("--dir",
35 action="store", type="string",
36 dest="dir",
37 help="")
38 self.OptionParser.add_option("--create-dir",
39 action="store", type="inkbool",
40 default=False,
41 dest="create_dir",
42 help="")
43 self.OptionParser.add_option("--with-code",
44 action="store", type="inkbool",
45 default=False,
46 dest="with_code",
47 help="")
50 svgNS = '{http://www.w3.org/2000/svg}'
53 def validate_inputs(self):
54 # The user must supply a directory to export:
55 if is_empty( self.options.dir ):
56 inkex.errormsg(_('You must to give a directory to export the slices.'))
57 return {'error':'You must to give a directory to export the slices.'}
58 # No directory separator at the path end:
59 if self.options.dir[-1] == '/' or self.options.dir[-1] == '\\':
60 self.options.dir = self.options.dir[0:-1]
61 # Test if the directory exists:
62 if not os.path.exists( self.options.dir ):
63 if self.options.create_dir:
64 # Try to create it:
65 try:
66 os.makedirs( self.options.dir )
67 except Exception as e:
68 inkex.errormsg( _('Can\'t create "%s".') % self.options.dir )
69 inkex.errormsg( _('Error: %s') % e )
70 return {'error':'Can\'t create the directory to export.'}
71 else:
72 inkex.errormsg(_('The directory "%s" does not exists.') % self.options.dir)
73 return
74 self.unique_html_id( self.get_slicer_layer() )
75 return None
78 _html_ids = []
79 def unique_html_id(self, el):
80 for child in el.getchildren():
81 if child.tag in [ self.svgNS+'rect', self.svgNS+'path',
82 self.svgNS+'circle', self.svgNS+'g' ]:
83 conf = self.get_el_conf(child)
84 if conf['html-id'] in self._html_ids:
85 inkex.errormsg(
86 _('You have more than one element with "%s" html-id.') %
87 conf['html-id'] )
88 n = 2
89 while (conf['html-id']+"-"+str(n)) in self._html_ids: n += 1
90 conf['html-id'] += "-"+str(n)
91 self._html_ids.append( conf['html-id'] )
92 self.save_conf( conf, child )
93 self.unique_html_id( child )
96 def test_if_has_imagemagick(self):
97 (status, output) = commands.getstatusoutput('convert --version')
98 self.has_magick = ( status == 0 and 'ImageMagick' in output )
101 def effect(self):
102 self.test_if_has_imagemagick()
103 error = self.validate_inputs()
104 if error: return error
105 # Register the basic CSS code:
106 self.reg_css( 'body', 'text-align', 'center' )
107 # Create the temporary SVG with invisible Slicer layer to export image pieces
108 self.create_the_temporary_svg()
109 # Start what we really want!
110 self.export_chids_of( self.get_slicer_layer() )
111 # Write the HTML and CSS files if asked for:
112 if self.options.with_code:
113 self.make_html_file()
114 self.make_css_file()
115 # Delete the temporary SVG with invisible Slicer layer
116 self.delete_the_temporary_svg()
117 #TODO: prevent inkex to return svg code to update Inkscape
120 def make_html_file(self):
121 f = open(os.path.join(self.options.dir,'layout.html'), 'w')
122 f.write(
123 '<html>\n<head>\n' +\
124 ' <title>Web Layout Testing</title>\n' +\
125 ' <style type="text/css" media="screen">\n' +\
126 ' @import url("style.css")\n' +\
127 ' </style>\n' +\
128 '</head>\n<body>\n' +\
129 self.html_code() +\
130 ' <p style="position:absolute; bottom:10px">\n' +\
131 ' This HTML code is not done to the web. <br/>\n' +\
132 ' The automatic HTML and CSS code are only a helper.' +\
133 '</p>\n</body>\n</html>' )
134 f.close()
137 def make_css_file(self):
138 f = open(os.path.join(self.options.dir,'style.css'), 'w')
139 f.write(
140 '/*\n' +\
141 '** This CSS code is not done to the web.\n' +\
142 '** The automatic HTML and CSS code are only a helper.\n' +\
143 '*/\n' +\
144 self.css_code() )
145 f.close()
148 def create_the_temporary_svg(self):
149 (ref, self.tmp_svg) = tempfile.mkstemp('.svg')
150 layer = self.get_slicer_layer()
151 current_style = ('style' in layer.attrib) and layer.attrib['style'] or ''
152 layer.attrib['style'] = 'display:none'
153 self.document.write( self.tmp_svg );
154 layer.attrib['style'] = current_style
157 def delete_the_temporary_svg(self):
158 os.remove( self.tmp_svg )
161 noid_element_count = 0
162 def get_el_conf(self, el):
163 desc = el.find(self.svgNS+'desc')
164 conf = {}
165 if desc is None:
166 desc = inkex.etree.SubElement(el, 'desc')
167 if desc.text is None:
168 desc.text = ''
169 for line in desc.text.split("\n"):
170 if line.find(':') > 0:
171 line = line.split(':')
172 conf[line[0].strip()] = line[1].strip()
173 if not 'html-id' in conf:
174 if el == self.get_slicer_layer():
175 return {'html-id':'#body#'}
176 else:
177 self.noid_element_count += 1
178 conf['html-id'] = 'element-'+str(self.noid_element_count)
179 desc.text += "\nhtml-id:"+conf['html-id']
180 return conf
183 def save_conf(self, conf, el):
184 desc = el.find('{http://www.w3.org/2000/svg}desc')
185 if desc is not None:
186 conf_a = []
187 for k in conf:
188 conf_a.append( k+' : '+conf[k] )
189 desc.text = "\n".join(conf_a)
192 def export_chids_of(self, parent):
193 parent_id = self.get_el_conf( parent )['html-id']
194 for el in parent.getchildren():
195 el_conf = self.get_el_conf( el )
196 if el.tag == self.svgNS+'g':
197 if self.options.with_code:
198 self.register_group_code( el, el_conf )
199 else:
200 self.export_chids_of( el )
201 if el.tag in [ self.svgNS+'rect', self.svgNS+'path', self.svgNS+'circle' ]:
202 if self.options.with_code:
203 self.register_unity_code( el, el_conf, parent_id )
204 self.export_img( el, el_conf )
207 def register_group_code(self, group, conf):
208 self.reg_html('div', group)
209 selec = '#'+conf['html-id']
210 self.reg_css( selec, 'position', 'absolute' )
211 geometry = self.get_relative_el_geometry(group)
212 self.reg_css( selec, 'top', str(int(geometry['y']))+'px' )
213 self.reg_css( selec, 'left', str(int(geometry['x']))+'px' )
214 self.reg_css( selec, 'width', str(int(geometry['w']))+'px' )
215 self.reg_css( selec, 'height', str(int(geometry['h']))+'px' )
216 self.export_chids_of( group )
219 def __validate_slice_conf(self, conf):
220 if not 'layout-disposition' in conf:
221 conf['layout-disposition'] = 'bg-el-norepeat'
222 if not 'layout-position-anchor' in conf:
223 conf['layout-position-anchor'] = 'mc'
224 return conf
227 def register_unity_code(self, el, conf, parent_id):
228 conf = self.__validate_slice_conf(conf)
229 css_selector = '#'+conf['html-id']
230 bg_repeat = 'no-repeat'
231 img_name = self.img_name(el, conf)
232 if conf['layout-disposition'][0:2] == 'bg':
233 if conf['layout-disposition'][0:9] == 'bg-parent':
234 if parent_id == '#body#':
235 css_selector = 'body'
236 else:
237 css_selector = '#'+parent_id
238 if conf['layout-disposition'] == 'bg-parent-repeat':
239 bg_repeat = 'repeat'
240 if conf['layout-disposition'] == 'bg-parent-repeat-x':
241 bg_repeat = 'repeat-x'
242 if conf['layout-disposition'] == 'bg-parent-repeat-y':
243 bg_repeat = 'repeat-y'
244 lay_anchor = conf['layout-position-anchor']
245 if lay_anchor == 'tl': lay_anchor = 'top left'
246 if lay_anchor == 'tc': lay_anchor = 'top center'
247 if lay_anchor == 'tr': lay_anchor = 'top right'
248 if lay_anchor == 'ml': lay_anchor = 'middle left'
249 if lay_anchor == 'mc': lay_anchor = 'middle center'
250 if lay_anchor == 'mr': lay_anchor = 'middle right'
251 if lay_anchor == 'bl': lay_anchor = 'bottom left'
252 if lay_anchor == 'bc': lay_anchor = 'bottom center'
253 if lay_anchor == 'br': lay_anchor = 'bottom right'
254 self.reg_css( css_selector, 'background',
255 'url("%s") %s %s' % (img_name, bg_repeat, lay_anchor) )
256 else: # conf['layout-disposition'][0:9] == 'bg-el...'
257 self.reg_html('div', el)
258 self.reg_css( css_selector, 'background',
259 'url("%s") %s' % (img_name, 'no-repeat') )
260 self.reg_css( css_selector, 'position', 'absolute' )
261 geo = self.get_relative_el_geometry(el,True)
262 self.reg_css( css_selector, 'top', geo['y'] )
263 self.reg_css( css_selector, 'left', geo['x'] )
264 self.reg_css( css_selector, 'width', geo['w'] )
265 self.reg_css( css_selector, 'height', geo['h'] )
266 else: # conf['layout-disposition'] == 'img...'
267 self.reg_html('img', el)
268 if conf['layout-disposition'] == 'img-pos':
269 self.reg_css( css_selector, 'position', 'absolute' )
270 geo = self.get_relative_el_geometry(el)
271 self.reg_css( css_selector, 'left', str(geo['x'])+'px' )
272 self.reg_css( css_selector, 'top', str(geo['y'])+'px' )
273 if conf['layout-disposition'] == 'img-float-left':
274 self.reg_css( css_selector, 'float', 'right' )
275 if conf['layout-disposition'] == 'img-float-right':
276 self.reg_css( css_selector, 'float', 'right' )
279 el_geo = { }
280 def register_all_els_geometry(self):
281 ink_cmm = 'inkscape --query-all '+self.tmp_svg
282 (status, output) = commands.getstatusoutput( ink_cmm )
283 self.el_geo = { }
284 if status == 0:
285 for el in output.split('\n'):
286 el = el.split(',')
287 if len(el) == 5:
288 self.el_geo[el[0]] = { 'x':float(el[1]), 'y':float(el[2]),
289 'w':float(el[3]), 'h':float(el[4]) }
290 doc_w = inkex.unittouu( self.document.getroot().get('width') )
291 doc_h = inkex.unittouu( self.document.getroot().get('height') )
292 self.el_geo['webslicer-layer'] = { 'x':0, 'y':0, 'w':doc_w, 'h':doc_h }
295 def get_relative_el_geometry(self, el, value_to_css=False):
296 # This method return a dictionary with x, y, w and h keys.
297 # All values are float, if value_to_css is False, otherwise
298 # that is a string ended with "px". The x and y values are
299 # relative to parent position.
300 if not self.el_geo: self.register_all_els_geometry()
301 parent = self.getParentNode(el)
302 geometry = self.el_geo[el.attrib['id']]
303 geometry['x'] -= self.el_geo[parent.attrib['id']]['x']
304 geometry['y'] -= self.el_geo[parent.attrib['id']]['y']
305 if value_to_css:
306 for k in geometry: geometry[k] = str(int(geometry[k]))+'px'
307 return geometry
310 def img_name(self, el, conf):
311 return el.attrib['id']+'.'+conf['format']
314 def export_img(self, el, conf):
315 if not self.has_magick:
316 inkex.errormsg(_('You must install the ImageMagick to get JPG and GIF.'))
317 conf['format'] = 'png'
318 img_name = os.path.join( self.options.dir, self.img_name(el, conf) )
319 img_name_png = img_name
320 if conf['format'] != 'png':
321 img_name_png = img_name+'.png'
322 opts = ''
323 if 'bg-color' in conf: opts += ' -b "'+conf['bg-color']+'" -y 1'
324 if 'dpi' in conf: opts += ' -d '+conf['dpi']
325 if 'dimension' in conf:
326 dim = conf['dimension'].split('x')
327 opts += ' -w '+dim[0]+' -h '+dim[1]
328 (status, output) = commands.getstatusoutput(
329 'inkscape %s -i "%s" -e "%s" "%s"' % (
330 opts, el.attrib['id'], img_name_png, self.tmp_svg
331 )
332 )
333 if conf['format'] != 'png':
334 opts = ''
335 if conf['format'] == 'jpg':
336 opts += ' -quality '+str(conf['quality'])
337 if conf['format'] == 'gif':
338 if conf['gif-type'] == 'grayscale':
339 opts += ' -type Grayscale'
340 else:
341 opts += ' -type Palette'
342 if conf['palette-size'] < 256:
343 opts += ' -colors '+str(conf['palette-size'])
344 (status, output) = commands.getstatusoutput(
345 'convert "%s" %s "%s"' % ( img_name_png, opts, img_name )
346 )
347 if status != 0:
348 inkex.errormsg('Upss... ImageMagick error: '+output)
349 os.remove(img_name_png)
352 _html = {}
353 def reg_html(self, el_tag, el):
354 parent = self.getParentNode( el )
355 parent_id = self.get_el_conf( parent )['html-id']
356 if parent == self.get_slicer_layer(): parent_id = 'body'
357 conf = self.get_el_conf( el )
358 el_id = conf['html-id']
359 if 'html-class' in conf:
360 el_class = conf['html-class']
361 else:
362 el_class = ''
363 if not parent_id in self._html: self._html[parent_id] = []
364 self._html[parent_id].append({ 'tag':el_tag, 'id':el_id, 'class':el_class })
367 def html_code(self, parent='body', ident=' '):
368 #inkex.errormsg( self._html )
369 if not parent in self._html: return ''
370 code = ''
371 for el in self._html[parent]:
372 child_code = self.html_code(el['id'], ident+' ')
373 tag_class = ''
374 if el['class'] != '': tag_class = ' class="'+el['class']+'"'
375 if el['tag'] == 'img':
376 code += ident+'<img id="'+el['id']+'"'+tag_class+\
377 ' src="'+self.img_name(el, self.get_el_conf(el))+'"/>\n'
378 else:
379 code += ident+'<'+el['tag']+' id="'+el['id']+'"'+tag_class+'>\n'
380 if child_code:
381 code += child_code
382 else:
383 code += ident+' Element '+el['id']+'\n'
384 code += ident+'</'+el['tag']+'><!-- id="'+el['id']+'" -->\n'
385 return code
388 _css = []
389 def reg_css(self, selector, att, val):
390 pos = i = -1
391 for s in self._css:
392 i += 1
393 if s['selector'] == selector: pos = i
394 if pos == -1:
395 pos = i + 1
396 self._css.append({ 'selector':selector, 'atts':{} })
397 if not att in self._css[pos]['atts']: self._css[pos]['atts'][att] = []
398 self._css[pos]['atts'][att].append( val )
401 def css_code(self):
402 code = ''
403 for s in self._css:
404 code += '\n'+s['selector']+' {\n'
405 for att in s['atts']:
406 val = s['atts'][att]
407 if att == 'background' and len(val) > 1:
408 code += ' /* the next attribute needs a CSS3 enabled browser */\n'
409 code += ' '+ att +': '+ (', '.join(val)) +';\n'
410 code += '}\n'
411 return code
414 def output(self):
415 # Cancel document serialization to stdout
416 pass
419 if __name__ == '__main__':
420 e = WebSlicer_Export()
421 e.affect()