ed99a3df215476a1693ae2f26b9dd4c7f5d7d754
[odoo/odoo.git] / bin / report / render / rml2txt / rml2txt.py
1 #!/bin/env python
2 # -*- encoding: utf-8 -*-
3 ##############################################################################
4 #
5 #    OpenERP, Open Source Management Solution   
6 #    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
7 #    $Id$
8 #
9 #    This program is free software: you can redistribute it and/or modify
10 #    it under the terms of the GNU General Public License as published by
11 #    the Free Software Foundation, either version 3 of the License, or
12 #    (at your option) any later version.
13 #
14 #    This program is distributed in the hope that it will be useful,
15 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
16 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 #    GNU General Public License for more details.
18 #
19 #    You should have received a copy of the GNU General Public License
20 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 #
22 ##############################################################################
23
24 # Copyright (C) 2005, Fabien Pinckaers, UCL, FSA
25 # Copyright (C) 2008, P. Christeas
26 #
27 # This library is free software; you can redistribute it and/or
28 # modify it under the terms of the GNU Lesser General Public
29 # License as published by the Free Software Foundation; either
30 # version 2.1 of the License, or (at your option) any later version.
31 #
32 # This library is distributed in the hope that it will be useful,
33 # but WITHOUT ANY WARRANTY; without even the implied warranty of
34 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
35 # Lesser General Public License for more details.
36 #
37 # You should have received a copy of the GNU Lesser General Public
38 # License along with this library; if not, write to the Free Software
39 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
40
41 import sys
42 import StringIO
43 import xml.dom.minidom
44 import copy
45
46 import utils
47
48 Font_size= 10.0
49
50 def verbose(text):
51         sys.stderr.write(text+"\n");
52
53 class textbox():
54         """A box containing plain text.
55         It can have an offset, in chars.
56         Lines can be either text strings, or textbox'es, recursively.
57         """
58         def __init__(self,x=0, y=0):
59             self.posx = x
60             self.posy = y
61             self.lines = []
62             self.curline = ''
63             self.endspace = False
64          
65         def newline(self):
66             if isinstance(self.curline, textbox):
67                 self.lines.extend(self.curline.renderlines())
68             else:
69                 self.lines.append(self.curline)
70             self.curline = ''
71         
72         def fline(self):
73             if isinstance(self.curline, textbox):
74                 self.lines.extend(self.curline.renderlines())
75             elif len(self.curline):
76                 self.lines.append(self.curline)
77             self.curline = ''
78         
79         def appendtxt(self,txt):
80             """Append some text to the current line.
81                Mimic the HTML behaviour, where all whitespace evaluates to
82                a single space """
83             bs = es = False
84             if txt[0].isspace():
85                 bs = True
86             if txt[len(txt)-1].isspace():
87                 es = True
88             if bs and not self.endspace:
89                 self.curline += " "
90             self.curline += txt.strip().replace("\n"," ").replace("\t"," ")
91             if es:
92                 self.curline += " "
93             self.endspace = es
94
95         def rendertxt(self,xoffset=0):
96             result = ''
97             lineoff = ""
98             for i in range(self.posy):
99                 result +="\n"
100             for i in range(self.posx+xoffset):
101                 lineoff+=" "
102             for l in self.lines:
103                 result+= lineoff+ l +"\n"
104             return result
105         
106         def renderlines(self,pad=0):
107             """Returns a list of lines, from the current object
108             pad: all lines must be at least pad characters.
109             """
110             result = []
111             lineoff = ""
112             for i in range(self.posx):
113                 lineoff+=" "
114             for l in self.lines:
115                 lpad = ""
116                 if pad and len(l) < pad :
117                         for i in range(pad - len(l)):
118                                 lpad += " "
119                 #elif pad and len(l) > pad ?
120                 result.append(lineoff+ l+lpad)
121             return result
122                         
123                         
124         def haplines(self,arr,offset,cc= ''):
125                 """ Horizontaly append lines 
126                 """
127                 while (len(self.lines) < len(arr)):
128                         self.lines.append("")
129                 
130                 for i in range(len(self.lines)):
131                         while (len(self.lines[i]) < offset):
132                                 self.lines[i] += " "
133                 for i in range(len(arr)):
134                         self.lines[i] += cc +arr[i] 
135                 
136
137 class _flowable(object):
138     def __init__(self, template, doc):
139         self._tags = {
140             '1title': self._tag_title,
141             '1spacer': self._tag_spacer,
142             'para': self._tag_para,
143             'font': self._tag_font,
144             'section': self._tag_section,
145             '1nextFrame': self._tag_next_frame,
146             'blockTable': self._tag_table,
147             '1pageBreak': self._tag_page_break,
148             '1setNextTemplate': self._tag_next_template,
149         }
150         self.template = template
151         self.doc = doc
152         self.nitags = []
153         self.tbox = None
154
155     def warn_nitag(self,tag):
156         if tag not in self.nitags:
157                 verbose("Unknown tag \"%s\", please implement it." % tag)
158                 self.nitags.append(tag)
159         
160     def _tag_page_break(self, node):
161         return "\f"
162
163     def _tag_next_template(self, node):
164         return ''
165
166     def _tag_next_frame(self, node):
167         result=self.template.frame_stop()
168         result+='\n'
169         result+=self.template.frame_start()
170         return result
171
172     def _tag_title(self, node):
173         node.tagName='h1'
174         return node.toxml()
175
176     def _tag_spacer(self, node):
177         length = 1+int(utils.unit_get(node.getAttribute('length')))/35
178         return "\n"*length
179
180     def _tag_table(self, node):
181         self.tb.fline()
182         saved_tb = self.tb
183         self.tb = None
184         sizes = None
185         if node.hasAttribute('colWidths'):
186             sizes = map(lambda x: utils.unit_get(x), node.getAttribute('colWidths').split(','))
187         trs = []
188         for n in node.childNodes:
189             if n.nodeType == node.ELEMENT_NODE and n.localName == 'tr':
190                 tds = []
191                 for m in n.childNodes:
192                     if m.nodeType == node.ELEMENT_NODE and m.localName == 'td':
193                         self.tb = textbox()
194                         self.rec_render_cnodes(m)
195                         tds.append(self.tb)
196                         self.tb = None
197                 if len(tds):
198                     trs.append(tds)
199         
200         if not sizes:
201                 verbose("computing table sizes..")
202         for tds in trs:
203                 trt = textbox()
204                 off=0
205                 for i in range(len(tds)):
206                         p = int(sizes[i]/Font_size)
207                         trl = tds[i].renderlines(pad=p)
208                         trt.haplines(trl,off)
209                         off += sizes[i]/Font_size
210                 saved_tb.curline = trt
211                 saved_tb.fline()
212         
213         self.tb = saved_tb
214         return
215
216     def _tag_para(self, node):
217         #TODO: styles
218         self.rec_render_cnodes(node)
219         self.tb.newline()
220
221     def _tag_section(self, node):
222         #TODO: styles
223         self.rec_render_cnodes(node)
224         self.tb.newline()
225
226     def _tag_font(self, node):
227         """We do ignore fonts.."""
228         self.rec_render_cnodes(node)
229
230     def rec_render_cnodes(self,node):
231         for n in node.childNodes:
232             self.rec_render(n)
233
234     def rec_render(self,node):
235         """ Recursive render: fill outarr with text of current node
236         """
237         if node.nodeType == node.TEXT_NODE:
238                 self.tb.appendtxt(node.data)
239         elif node.nodeType==node.ELEMENT_NODE:
240                 if node.localName in self._tags:
241                     self._tags[node.localName](node)
242                 else:
243                     self.warn_nitag(node.localName)
244         else:
245                 verbose("Unknown nodeType: %d" % node.nodeType)
246
247     def render(self, node):
248         self.tb= textbox()
249         #result = self.template.start()
250         #result += self.template.frame_start()
251         self.rec_render_cnodes(node)
252         #result += self.template.frame_stop()
253         #result += self.template.end()
254         result = self.tb.rendertxt()
255         del self.tb
256         return result
257
258 class _rml_tmpl_tag(object):
259     def __init__(self, *args):
260         pass
261     def tag_start(self):
262         return ''
263     def tag_end(self):
264         return False
265     def tag_stop(self):
266         return ''
267     def tag_mergeable(self):
268         return True
269
270 class _rml_tmpl_frame(_rml_tmpl_tag):
271     def __init__(self, posx, width):
272         self.width = width
273         self.posx = posx
274     def tag_start(self):
275         return "frame start"
276         return '<table border="0" width="%d"><tr><td width="%d">&nbsp;</td><td>' % (self.width+self.posx,self.posx)
277     def tag_end(self):
278         return True
279     def tag_stop(self):
280         return "frame stop"
281         return '</td></tr></table><br/>'
282     def tag_mergeable(self):
283         return False
284
285     # An awfull workaround since I don't really understand the semantic behind merge.
286     def merge(self, frame):
287         pass
288
289 class _rml_tmpl_draw_string(_rml_tmpl_tag):
290     def __init__(self, node, style):
291         self.posx = utils.unit_get(node.getAttribute('x'))
292         self.posy =  utils.unit_get(node.getAttribute('y'))
293         aligns = {
294             'drawString': 'left',
295             'drawRightString': 'right',
296             'drawCentredString': 'center'
297         }
298         align = aligns[node.localName]
299         self.pos = [(self.posx, self.posy, align, utils.text_get(node), style.get('td'), style.font_size_get('td'))]
300
301     def tag_start(self):
302         return "draw string \"%s\" @(%d,%d)..\n" %("txt",self.posx,self.posy)
303         self.pos.sort()
304         res = '\\table ...'
305         posx = 0
306         i = 0
307         for (x,y,align,txt, style, fs) in self.pos:
308             if align=="left":
309                 pos2 = len(txt)*fs
310                 res+='<td width="%d"></td><td style="%s" width="%d">%s</td>' % (x - posx, style, pos2, txt)
311                 posx = x+pos2
312             if align=="right":
313                 res+='<td width="%d" align="right" style="%s">%s</td>' % (x - posx, style, txt)
314                 posx = x
315             if align=="center":
316                 res+='<td width="%d" align="center" style="%s">%s</td>' % ((x - posx)*2, style, txt)
317                 posx = 2*x-posx
318             i+=1
319         res+='\\table end'
320         return res
321     def merge(self, ds):
322         self.pos+=ds.pos
323
324 class _rml_tmpl_draw_lines(_rml_tmpl_tag):
325     def __init__(self, node, style):
326         coord = [utils.unit_get(x) for x in utils.text_get(node).split(' ')]
327         self.ok = False
328         self.posx = coord[0]
329         self.posy = coord[1]
330         self.width = coord[2]-coord[0]
331         self.ok = coord[1]==coord[3]
332         self.style = style
333         self.style = style.get('hr')
334
335     def tag_start(self):
336         return "draw lines..\n"
337         if self.ok:
338             return '<table border="0" cellpadding="0" cellspacing="0" width="%d"><tr><td width="%d"></td><td><hr width="100%%" style="margin:0px; %s"></td></tr></table>' % (self.posx+self.width,self.posx,self.style)
339         else:
340             return ''
341
342 class _rml_stylesheet(object):
343     def __init__(self, stylesheet, doc):
344         self.doc = doc
345         self.attrs = {}
346         self._tags = {
347             'fontSize': lambda x: ('font-size',str(utils.unit_get(x))+'px'),
348             'alignment': lambda x: ('text-align',str(x))
349         }
350         result = ''
351         for ps in stylesheet.getElementsByTagName('paraStyle'):
352             attr = {}
353             attrs = ps.attributes
354             for i in range(attrs.length):
355                  name = attrs.item(i).localName
356                  attr[name] = ps.getAttribute(name)
357             attrs = []
358             for a in attr:
359                 if a in self._tags:
360                     attrs.append("%s:%s" % self._tags[a](attr[a]))
361             if len(attrs):
362                 result += "p."+attr['name']+" {"+'; '.join(attrs)+"}\n"
363         self.result = result
364
365     def render(self):
366         return ''
367
368 class _rml_draw_style(object):
369     def __init__(self):
370         self.style = {}
371         self._styles = {
372             'fill': lambda x: {'td': {'color':x.getAttribute('color')}},
373             'setFont': lambda x: {'td': {'font-size':x.getAttribute('size')+'px'}},
374             'stroke': lambda x: {'hr': {'color':x.getAttribute('color')}},
375         }
376     def update(self, node):
377         if node.localName in self._styles:
378             result = self._styles[node.localName](node)
379             for key in result:
380                 if key in self.style:
381                     self.style[key].update(result[key])
382                 else:
383                     self.style[key] = result[key]
384     def font_size_get(self,tag):
385         size  = utils.unit_get(self.style.get('td', {}).get('font-size','16'))
386         return size
387
388     def get(self,tag):
389         if not tag in self.style:
390             return ""
391         return ';'.join(['%s:%s' % (x[0],x[1]) for x in self.style[tag].items()])
392
393 class _rml_template(object):
394     def __init__(self, template):
395         self.frame_pos = -1
396         self.frames = []
397         self.template_order = []
398         self.page_template = {}
399         self.loop = 0
400         self._tags = {
401             'drawString': _rml_tmpl_draw_string,
402             'drawRightString': _rml_tmpl_draw_string,
403             'drawCentredString': _rml_tmpl_draw_string,
404             'lines': _rml_tmpl_draw_lines
405         }
406         self.style = _rml_draw_style()
407         for pt in template.getElementsByTagName('pageTemplate'):
408             frames = {}
409             id = pt.getAttribute('id')
410             self.template_order.append(id)
411             for tmpl in pt.getElementsByTagName('frame'):
412                 posy = int(utils.unit_get(tmpl.getAttribute('y1'))) #+utils.unit_get(tmpl.getAttribute('height')))
413                 posx = int(utils.unit_get(tmpl.getAttribute('x1')))
414                 frames[(posy,posx,tmpl.getAttribute('id'))] = _rml_tmpl_frame(posx, utils.unit_get(tmpl.getAttribute('width')))
415             for tmpl in template.getElementsByTagName('pageGraphics'):
416                 for n in tmpl.childNodes:
417                     if n.nodeType==n.ELEMENT_NODE:
418                         if n.localName in self._tags:
419                             t = self._tags[n.localName](n, self.style)
420                             frames[(t.posy,t.posx,n.localName)] = t
421                         else:
422                             self.style.update(n)
423             keys = frames.keys()
424             keys.sort()
425             keys.reverse()
426             self.page_template[id] = []
427             for key in range(len(keys)):
428                 if key>0 and keys[key-1][0] == keys[key][0]:
429                     if type(self.page_template[id][-1]) == type(frames[keys[key]]):
430                         if self.page_template[id][-1].tag_mergeable():
431                             self.page_template[id][-1].merge(frames[keys[key]])
432                         continue
433                 self.page_template[id].append(frames[keys[key]])
434         self.template = self.template_order[0]
435
436     def _get_style(self):
437         return self.style
438
439     def set_next_template(self):
440         self.template = self.template_order[(self.template_order.index(name)+1) % self.template_order]
441         self.frame_pos = -1
442
443     def set_template(self, name):
444         self.template = name
445         self.frame_pos = -1
446
447     def frame_start(self):
448         result = ''
449         frames = self.page_template[self.template]
450         ok = True
451         while ok:
452             self.frame_pos += 1
453             if self.frame_pos>=len(frames):
454                 self.frame_pos=0
455                 self.loop=1
456                 ok = False
457                 continue
458             f = frames[self.frame_pos]
459             result+=f.tag_start()
460             ok = not f.tag_end()
461             if ok:
462                 result+=f.tag_stop()
463         return result
464
465     def frame_stop(self):
466         frames = self.page_template[self.template]
467         f = frames[self.frame_pos]
468         result=f.tag_stop()
469         return result
470
471     def start(self):
472         return ''
473     
474     def end(self):
475         return "template end\n"
476         result = ''
477         while not self.loop:
478             result += self.frame_start()
479             result += self.frame_stop()
480         return result
481
482 class _rml_doc(object):
483     def __init__(self, data):
484         self.dom = xml.dom.minidom.parseString(data)
485         self.filename = self.dom.documentElement.getAttribute('filename')
486         self.result = ''
487
488     def render(self, out):
489         template = _rml_template(self.dom.documentElement.getElementsByTagName('template')[0])
490         f = _flowable(template, self.dom)
491         self.result += f.render(self.dom.documentElement.getElementsByTagName('story')[0])
492         del f
493         self.result += '\n'
494         out.write( self.result)
495
496 def parseString(data, fout=None):
497     r = _rml_doc(data)
498     if fout:
499         fp = file(fout,'wb')
500         r.render(fp)
501         fp.close()
502         return fout
503     else:
504         fp = StringIO.StringIO()
505         r.render(fp)
506         return fp.getvalue()
507
508 def trml2pdf_help():
509     print 'Usage: rml2txt input.rml >output.html'
510     print 'Render the standard input (RML) and output an TXT file'
511     sys.exit(0)
512
513 if __name__=="__main__":
514     if len(sys.argv)>1:
515         if sys.argv[1]=='--help':
516             trml2pdf_help()
517         print parseString(file(sys.argv[1], 'r').read()).encode('iso8859-7')
518     else:
519         print 'Usage: trml2txt input.rml >output.pdf'
520         print 'Try \'trml2txt --help\' for more information.'
521
522 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
523