Code

Simple first pass for rough timing
[inkscape.git] / share / extensions / webslicer_export.py
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
24 import sys
25 import tempfile
27 _ = gettext.gettext
30 class WebSlicer_Export(WebSlicer_Effect):
32     def __init__(self):
33         WebSlicer_Effect.__init__(self)
34         self.OptionParser.add_option("--tab")
35         self.OptionParser.add_option("--dir",
36                                      action="store", type="string",
37                                      dest="dir",
38                                      help="")
39         self.OptionParser.add_option("--create-dir",
40                                      action="store", type="inkbool",
41                                      default=False,
42                                      dest="create_dir",
43                                      help="")
44         self.OptionParser.add_option("--with-code",
45                                      action="store", type="inkbool",
46                                      default=False,
47                                      dest="with_code",
48                                      help="")
51     svgNS = '{http://www.w3.org/2000/svg}'
54     def validate_inputs(self):
55         # The user must supply a directory to export:
56         if is_empty( self.options.dir ):
57             inkex.errormsg(_('You must to give a directory to export the slices.'))
58             return {'error':'You must to give a directory to export the slices.'}
59         # No directory separator at the path end:
60         if self.options.dir[-1] == '/' or self.options.dir[-1] == '\\':
61             self.options.dir = self.options.dir[0:-1]
62         # Test if the directory exists:
63         if not os.path.exists( self.options.dir ):
64             if self.options.create_dir:
65                 # Try to create it:
66                 try:
67                     os.makedirs( self.options.dir )
68                 except Exception, e:
69                     inkex.errormsg( _('Can\'t create "%s".') % self.options.dir )
70                     inkex.errormsg( _('Error: %s') % e )
71                     return {'error':'Can\'t create the directory to export.'}
72             else:
73                 inkex.errormsg(_('The directory "%s" does not exists.') % self.options.dir)
74                 return
75         self.unique_html_id( self.get_slicer_layer() )
76         return None
79     def get_cmd_output(self, cmd):
80         # This solution comes from Andrew Reedick <jr9445 at ATT.COM>
81         # http://mail.python.org/pipermail/python-win32/2008-January/006606.html
82         # This method replaces the commands.getstatusoutput() usage, with the
83         # hope to correct the windows exporting bug:
84         # https://bugs.launchpad.net/inkscape/+bug/563722
85         if sys.platform != "win32": cmd = '{ '+ cmd +'; }'
86         pipe = os.popen(cmd +' 2>&1', 'r')
87         text = pipe.read()
88         sts = pipe.close()
89         if sts is None: sts = 0
90         if text[-1:] == '\n': text = text[:-1]
91         return sts, text
94     _html_ids = []
95     def unique_html_id(self, el):
96         for child in el.getchildren():
97             if child.tag in [ self.svgNS+'rect', self.svgNS+'path',
98                               self.svgNS+'circle', self.svgNS+'g' ]:
99                 conf = self.get_el_conf(child)
100                 if conf['html-id'] in self._html_ids:
101                     inkex.errormsg(
102                         _('You have more than one element with "%s" html-id.') %
103                         conf['html-id'] )
104                     n = 2
105                     while (conf['html-id']+"-"+str(n)) in self._html_ids: n += 1
106                     conf['html-id'] += "-"+str(n)
107                 self._html_ids.append( conf['html-id'] )
108                 self.save_conf( conf, child )
109                 self.unique_html_id( child )
112     def test_if_has_imagemagick(self):
113         (status, output) = self.get_cmd_output('convert --version')
114         self.has_magick = ( status == 0 and 'ImageMagick' in output )
117     def effect(self):
118         self.test_if_has_imagemagick()
119         error = self.validate_inputs()
120         if error: return error
121         # Register the basic CSS code:
122         self.reg_css( 'body', 'text-align', 'center' )
123         # Create the temporary SVG with invisible Slicer layer to export image pieces
124         self.create_the_temporary_svg()
125         # Start what we really want!
126         self.export_chids_of( self.get_slicer_layer() )
127         # Write the HTML and CSS files if asked for:
128         if self.options.with_code:
129             self.make_html_file()
130             self.make_css_file()
131         # Delete the temporary SVG with invisible Slicer layer
132         self.delete_the_temporary_svg()
133         #TODO: prevent inkex to return svg code to update Inkscape
136     def make_html_file(self):
137         f = open(os.path.join(self.options.dir,'layout.html'), 'w')
138         f.write(
139             '<html>\n<head>\n'                                     +\
140             '  <title>Web Layout Testing</title>\n'                +\
141             '  <style type="text/css" media="screen">\n'           +\
142             '    @import url("style.css")\n'                       +\
143             '  </style>\n'                                         +\
144             '</head>\n<body>\n'                                    +\
145             self.html_code()                                       +\
146             '  <p style="position:absolute; bottom:10px">\n'       +\
147             '  This HTML code is not done to the web. <br/>\n'     +\
148             '  The automatic HTML and CSS code are only a helper.' +\
149             '</p>\n</body>\n</html>' )
150         f.close()
153     def make_css_file(self):
154         f = open(os.path.join(self.options.dir,'style.css'), 'w')
155         f.write(
156             '/*\n'                                                    +\
157             '** This CSS code is not done to the web.\n'              +\
158             '** The automatic HTML and CSS code are only a helper.\n' +\
159             '*/\n'                                                    +\
160             self.css_code() )
161         f.close()
164     def create_the_temporary_svg(self):
165         (ref, self.tmp_svg) = tempfile.mkstemp('.svg')
166         layer = self.get_slicer_layer()
167         current_style = ('style' in layer.attrib) and layer.attrib['style'] or ''
168         layer.attrib['style'] = 'display:none'
169         self.document.write( self.tmp_svg );
170         layer.attrib['style'] = current_style
173     def delete_the_temporary_svg(self):
174         os.remove( self.tmp_svg )
177     noid_element_count = 0
178     def get_el_conf(self, el):
179         desc = el.find(self.svgNS+'desc')
180         conf = {}
181         if desc is None:
182             desc = inkex.etree.SubElement(el, 'desc')
183         if desc.text is None:
184             desc.text = ''
185         for line in desc.text.split("\n"):
186             if line.find(':') > 0:
187                 line = line.split(':')
188                 conf[line[0].strip()] = line[1].strip()
189         if not 'html-id' in conf:
190             if el == self.get_slicer_layer():
191                 return {'html-id':'#body#'}
192             else:
193                 self.noid_element_count += 1
194                 conf['html-id'] = 'element-'+str(self.noid_element_count)
195                 desc.text += "\nhtml-id:"+conf['html-id']
196         return conf
199     def save_conf(self, conf, el):
200         desc = el.find('{http://www.w3.org/2000/svg}desc')
201         if desc is not None:
202             conf_a = []
203             for k in conf:
204                 conf_a.append( k+' : '+conf[k] )
205             desc.text = "\n".join(conf_a)
208     def export_chids_of(self, parent):
209         parent_id = self.get_el_conf( parent )['html-id']
210         for el in parent.getchildren():
211             el_conf = self.get_el_conf( el )
212             if el.tag == self.svgNS+'g':
213                 if self.options.with_code:
214                     self.register_group_code( el, el_conf )
215                 else:
216                     self.export_chids_of( el )
217             if el.tag in [ self.svgNS+'rect', self.svgNS+'path', self.svgNS+'circle' ]:
218                 if self.options.with_code:
219                     self.register_unity_code( el, el_conf, parent_id )
220                 self.export_img( el, el_conf )
223     def register_group_code(self, group, conf):
224         self.reg_html('div', group)
225         selec = '#'+conf['html-id']
226         self.reg_css( selec, 'position', 'absolute' )
227         geometry = self.get_relative_el_geometry(group)
228         self.reg_css( selec, 'top', str(int(geometry['y']))+'px' )
229         self.reg_css( selec, 'left', str(int(geometry['x']))+'px' )
230         self.reg_css( selec, 'width', str(int(geometry['w']))+'px' )
231         self.reg_css( selec, 'height', str(int(geometry['h']))+'px' )
232         self.export_chids_of( group )
235     def __validate_slice_conf(self, conf):
236         if not 'layout-disposition' in conf:
237             conf['layout-disposition'] = 'bg-el-norepeat'
238         if not 'layout-position-anchor' in conf:
239             conf['layout-position-anchor'] = 'mc'
240         return conf
243     def register_unity_code(self, el, conf, parent_id):
244         conf = self.__validate_slice_conf(conf)
245         css_selector = '#'+conf['html-id']
246         bg_repeat = 'no-repeat'
247         img_name = self.img_name(el, conf)
248         if conf['layout-disposition'][0:2] == 'bg':
249             if conf['layout-disposition'][0:9] == 'bg-parent':
250                 if parent_id == '#body#':
251                     css_selector = 'body'
252                 else:
253                     css_selector = '#'+parent_id
254                 if conf['layout-disposition'] == 'bg-parent-repeat':
255                     bg_repeat = 'repeat'
256                 if conf['layout-disposition'] == 'bg-parent-repeat-x':
257                     bg_repeat = 'repeat-x'
258                 if conf['layout-disposition'] == 'bg-parent-repeat-y':
259                     bg_repeat = 'repeat-y'
260                 lay_anchor = conf['layout-position-anchor']
261                 if lay_anchor == 'tl': lay_anchor = 'top left'
262                 if lay_anchor == 'tc': lay_anchor = 'top center'
263                 if lay_anchor == 'tr': lay_anchor = 'top right'
264                 if lay_anchor == 'ml': lay_anchor = 'middle left'
265                 if lay_anchor == 'mc': lay_anchor = 'middle center'
266                 if lay_anchor == 'mr': lay_anchor = 'middle right'
267                 if lay_anchor == 'bl': lay_anchor = 'bottom left'
268                 if lay_anchor == 'bc': lay_anchor = 'bottom center'
269                 if lay_anchor == 'br': lay_anchor = 'bottom right'
270                 self.reg_css( css_selector, 'background',
271                               'url("%s") %s %s' % (img_name, bg_repeat, lay_anchor) )
272             else: # conf['layout-disposition'][0:9] == 'bg-el...'
273                 self.reg_html('div', el)
274                 self.reg_css( css_selector, 'background',
275                               'url("%s") %s' % (img_name, 'no-repeat') )
276                 self.reg_css( css_selector, 'position', 'absolute' )
277                 geo = self.get_relative_el_geometry(el,True)
278                 self.reg_css( css_selector, 'top', geo['y'] )
279                 self.reg_css( css_selector, 'left', geo['x'] )
280                 self.reg_css( css_selector, 'width', geo['w'] )
281                 self.reg_css( css_selector, 'height', geo['h'] )
282         else: # conf['layout-disposition'] == 'img...'
283                 self.reg_html('img', el)
284                 if conf['layout-disposition'] == 'img-pos':
285                     self.reg_css( css_selector, 'position', 'absolute' )
286                     geo = self.get_relative_el_geometry(el)
287                     self.reg_css( css_selector, 'left', str(geo['x'])+'px' )
288                     self.reg_css( css_selector, 'top', str(geo['y'])+'px' )
289                 if conf['layout-disposition'] == 'img-float-left':
290                     self.reg_css( css_selector, 'float', 'right' )
291                 if conf['layout-disposition'] == 'img-float-right':
292                     self.reg_css( css_selector, 'float', 'right' )
295     el_geo = { }
296     def register_all_els_geometry(self):
297         ink_cmm = 'inkscape --query-all '+self.tmp_svg
298         (status, output) = self.get_cmd_output( ink_cmm )
299         self.el_geo = { }
300         if status == 0:
301             for el in output.split('\n'):
302                 el = el.split(',')
303                 if len(el) == 5:
304                     self.el_geo[el[0]] = { 'x':float(el[1]), 'y':float(el[2]),
305                                            'w':float(el[3]), 'h':float(el[4]) }
306         doc_w = inkex.unittouu( self.document.getroot().get('width') )
307         doc_h = inkex.unittouu( self.document.getroot().get('height') )
308         self.el_geo['webslicer-layer'] = { 'x':0, 'y':0, 'w':doc_w, 'h':doc_h }
311     def get_relative_el_geometry(self, el, value_to_css=False):
312         # This method return a dictionary with x, y, w and h keys.
313         # All values are float, if value_to_css is False, otherwise
314         # that is a string ended with "px". The x and y values are
315         # relative to parent position.
316         if not self.el_geo: self.register_all_els_geometry()
317         parent = self.getParentNode(el)
318         geometry = self.el_geo[el.attrib['id']]
319         geometry['x'] -= self.el_geo[parent.attrib['id']]['x']
320         geometry['y'] -= self.el_geo[parent.attrib['id']]['y']
321         if value_to_css:
322             for k in geometry: geometry[k] = str(int(geometry[k]))+'px'
323         return geometry
326     def img_name(self, el, conf):
327         return el.attrib['id']+'.'+conf['format']
330     def export_img(self, el, conf):
331         if not self.has_magick:
332             inkex.errormsg(_('You must install the ImageMagick to get JPG and GIF.'))
333             conf['format'] = 'png'
334         img_name = os.path.join( self.options.dir, self.img_name(el, conf) )
335         img_name_png = img_name
336         if conf['format'] != 'png':
337             img_name_png = img_name+'.png'
338         opts = ''
339         if 'bg-color' in conf: opts += ' -b "'+conf['bg-color']+'" -y 1'
340         if 'dpi' in conf: opts += ' -d '+conf['dpi']
341         if 'dimension' in conf:
342             dim = conf['dimension'].split('x')
343             opts += ' -w '+dim[0]+' -h '+dim[1]
344         (status, output) = self.get_cmd_output(
345             'inkscape %s -i "%s" -e "%s" "%s"' % (
346                 opts, el.attrib['id'], img_name_png, self.tmp_svg
347             )
348         )
349         if conf['format'] != 'png':
350             opts = ''
351             if conf['format'] == 'jpg':
352                 opts += ' -quality '+str(conf['quality'])
353             if conf['format'] == 'gif':
354                 if conf['gif-type'] == 'grayscale':
355                     opts += ' -type Grayscale'
356                 else:
357                     opts += ' -type Palette'
358                 if conf['palette-size'] < 256:
359                     opts += ' -colors '+str(conf['palette-size'])
360             (status, output) = self.get_cmd_output(
361                 'convert "%s" %s "%s"' % ( img_name_png, opts, img_name )
362             )
363             if status != 0:
364                 inkex.errormsg('Upss... ImageMagick error: '+output)
365             os.remove(img_name_png)
368     _html = {}
369     def reg_html(self, el_tag, el):
370         parent = self.getParentNode( el )
371         parent_id = self.get_el_conf( parent )['html-id']
372         if parent == self.get_slicer_layer(): parent_id = 'body'
373         conf = self.get_el_conf( el )
374         el_id = conf['html-id']
375         if 'html-class' in conf:
376           el_class = conf['html-class']
377         else:
378           el_class = ''
379         if not parent_id in self._html: self._html[parent_id] = []
380         self._html[parent_id].append({ 'tag':el_tag, 'id':el_id, 'class':el_class })
383     def html_code(self, parent='body', ident='  '):
384         #inkex.errormsg( self._html )
385         if not parent in self._html: return ''
386         code = ''
387         for el in self._html[parent]:
388             child_code = self.html_code(el['id'], ident+'  ')
389             tag_class = ''
390             if el['class'] != '': tag_class = ' class="'+el['class']+'"'
391             if el['tag'] == 'img':
392                 code += ident+'<img id="'+el['id']+'"'+tag_class+\
393                         ' src="'+self.img_name(el, self.get_el_conf(el))+'"/>\n'
394             else:
395                 code += ident+'<'+el['tag']+' id="'+el['id']+'"'+tag_class+'>\n'
396                 if child_code:
397                     code += child_code
398                 else:
399                     code += ident+'  Element '+el['id']+'\n'
400                 code += ident+'</'+el['tag']+'><!-- id="'+el['id']+'" -->\n'
401         return code
404     _css = []
405     def reg_css(self, selector, att, val):
406         pos = i = -1
407         for s in self._css:
408             i += 1
409             if s['selector'] == selector: pos = i
410         if pos == -1:
411             pos = i + 1
412             self._css.append({ 'selector':selector, 'atts':{} })
413         if not att in self._css[pos]['atts']: self._css[pos]['atts'][att] = []
414         self._css[pos]['atts'][att].append( val )
417     def css_code(self):
418         code = ''
419         for s in self._css:
420             code += '\n'+s['selector']+' {\n'
421             for att in s['atts']:
422                 val = s['atts'][att]
423                 if att == 'background' and len(val) > 1:
424                     code += '  /* the next attribute needs a CSS3 enabled browser */\n'
425                 code += '  '+ att +': '+ (', '.join(val)) +';\n'
426             code += '}\n'
427         return code
430     def output(self):
431         # Cancel document serialization to stdout
432         pass
435 if __name__ == '__main__':
436     e = WebSlicer_Export()
437     e.affect()