root/relatorio/templates/opendocument.py @ 58:1180376ab3cf

Revision 58:1180376ab3cf, 12.8 kB (checked in by Nicolas ?vrard <nicoe@…>, 5 years ago)

Added feature to use image report in odf report
Made the demo_*py files importable

Line 
1###############################################################################
2#
3# Copyright (c) 2007, 2008 OpenHex SPRL. (http://openhex.com) All Rights
4# Reserved.
5#
6# This program is free software; you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation; either version 2 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program.  If not, see <http://www.gnu.org/licenses/>.
18#
19###############################################################################
20
21__metaclass__ = type
22
23import os
24import re
25import md5
26import urllib
27import zipfile
28from cStringIO import StringIO
29
30import warnings
31warnings.filterwarnings('always', module='relatorio.templates.opendocument')
32
33import lxml.etree
34import genshi
35import genshi.output
36from genshi.template import MarkupTemplate
37from relatorio.reporting import Report
38
39GENSHI_EXPR = re.compile(r'''((/)?(for|choose|otherwise|when|if|with)( (\w+)=["'](.*)["']|)|.*)''')
40EXTENSIONS = {'image/png': 'png',
41              'image/jpeg': 'jpg',
42              'image/bmp': 'bmp',
43              'image/gif': 'gif',
44              'image/tiff': 'tif',
45              'image/xbm': 'xbm',
46             }
47
48_encode = genshi.output.encode
49ETElement = lxml.etree.Element
50
51
52class OOTemplateError(genshi.template.base.TemplateSyntaxError):
53    pass
54
55
56class ImageHref:
57   
58    def __init__(self, zipfile, context):
59        self.zip = zipfile
60        self.context = context.copy()
61        self.obj = self.context.pop('o')
62
63    def __call__(self, expr, name):
64        bitstream, mimetype = expr
65        if isinstance(bitstream, Report):
66            bitstream = bitstream(self.obj, **self.context).render()
67        bitstream.seek(0)
68        file_content = bitstream.read()
69        name = md5.new(file_content).hexdigest()
70        path = 'Pictures/%s.%s' % (name, EXTENSIONS[mimetype])
71        if path not in self.zip.namelist():
72            self.zip.writestr(path, file_content)
73        return {'{http://www.w3.org/1999/xlink}href': path}
74
75
76class Template(MarkupTemplate):
77
78    def __init__(self, source, filepath=None, filename=None, loader=None,
79                 encoding=None, lookup='strict', allow_exec=True):
80        self.namespaces = {}
81        self.inner_docs = []
82        super(Template, self).__init__(source, filepath, filename, loader,
83                                       encoding, lookup, allow_exec)
84
85    def _parse(self, source, encoding):
86        inzip = zipfile.ZipFile(self.filepath)
87        content = inzip.read('content.xml')
88        styles = inzip.read('styles.xml')
89
90        genshi_obj = super(Template, self)
91        content = genshi_obj._parse(self.add_directives(content), encoding)
92        styles = genshi_obj._parse(self.add_directives(styles), encoding)
93        content_files= [('content.xml', content)]
94        styles_files = [('styles.xml', styles)]
95
96        while self.inner_docs:
97            doc = self.inner_docs.pop()
98            c_path, s_path = doc + '/content.xml', doc + '/styles.xml'
99            content = inzip.read(c_path)
100            styles = inzip.read(s_path)
101           
102            c_parsed = genshi_obj._parse(self.add_directives(content), encoding)
103            s_parsed = genshi_obj._parse(self.add_directives(styles), encoding)
104
105            content_files.append((c_path, c_parsed))
106            styles_files.append((s_path, s_parsed))
107
108        inzip.close()
109        parsed = []
110        for fpath, fparsed in content_files + styles_files:
111            parsed.append((genshi.core.PI, ('relatorio', fpath), None))
112            parsed += fparsed
113
114        return parsed
115
116    def add_directives(self, content):
117        tree = lxml.etree.parse(StringIO(content))
118        root = tree.getroot()
119        self.namespaces = root.nsmap.copy()
120        self.namespaces['py'] = 'http://genshi.edgewall.org/'
121
122        self._invert_style(tree)
123        self._handle_relatorio_tags(tree)
124        self._handle_images(tree)
125        self._handle_innerdocs(tree)
126        return StringIO(lxml.etree.tostring(tree))
127
128    def _invert_style(self, tree):
129        xpath_expr = "//text:a[starts-with(@xlink:href, 'relatorio://')]"\
130                     "/text:span"
131        for span in tree.xpath(xpath_expr, namespaces=self.namespaces):
132            text_a = span.getparent()
133            outer = text_a.getparent()
134            text_a.text = span.text
135            span.text = ''
136            text_a.remove(span)
137            outer.replace(text_a, span)
138            span.append(text_a)
139
140    def _relatorio_statements(self, tree):
141        # If this node href matches the relatorio URL it is kept.
142        # If this node href matches a genshi directive it is kept for further
143        # processing.
144        r_statements, genshi_dir = [], []
145        xlink_href_attrib = '{%s}href' % self.namespaces['xlink']
146        text_a = '{%s}a' % self.namespaces['text']
147        placeholder = '{%s}placeholder' % self.namespaces['text']
148
149        s_xpath = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \
150                  "| //text:placeholder"
151        for statement in tree.xpath(s_xpath, namespaces=self.namespaces):
152            if statement.tag == placeholder:
153                expr = statement.text[1:-1]
154            elif statement.tag == text_a:
155                expr = urllib.unquote(statement.attrib[xlink_href_attrib][12:])
156
157            if not expr:
158                raise OOTemplateError("No expression in the tag",
159                                      self.filepath)
160            elif not statement.text:
161                warnings.warn('No statement text in %s' % self.filepath)
162            elif expr != statement.text and statement.tag == text_a:
163                warnings.warn('url and text do not match in %s: %s != %s' 
164                              % (self.filepath, expr,
165                                 statement.text.encode('utf-8')))
166
167            match_obj = GENSHI_EXPR.match(expr)
168
169            expr, closing, directive, _, attr, attr_val = match_obj.groups()
170            if directive is not None:
171                genshi_dir.append((statement, closing))
172            r_statements.append((statement, 
173                                 (expr, closing, directive, attr, attr_val)))
174
175        return r_statements, genshi_dir
176
177    def _handle_relatorio_tags(self, tree):
178        """
179        Will treat all relatorio tag (py:if/for/choose/when/otherwise)
180        tags
181        """
182        # Some tag name constants
183        table_cell_tag = '{%s}table-cell' % self.namespaces['table']
184        attrib_name = '{%s}attrs' % self.namespaces['py']
185        office_name = '{%s}value' % self.namespaces['office']
186        office_valuetype = '{%s}value-type' % self.namespaces['office']
187        genshi_replace = '{%s}replace' % self.namespaces['py']
188
189        r_statements, genshi_directives = self._relatorio_statements(tree)
190        # We match the opening and closing directives together
191        idx = 0
192        genshi_pairs, inserted = [], []
193        for statement, closing in genshi_directives:
194            if closing is None:
195                genshi_pairs.append([statement, None])
196                inserted.append(idx)
197                idx += 1
198            else:
199                genshi_pairs[inserted.pop()][1] = statement
200
201        for r_node, parsed in r_statements:
202            expr, c_dir, directive, attr, a_val = parsed
203
204            if directive is not None:
205                # If the node is a genshi directive statement:
206                #    - we operate only on opening statement
207                #    - we find the nearest ancestor of the closing and opening
208                #      statement
209                #    - we create a <py:xxx> node
210                #    - we add all the node between the opening and closing
211                #      statements to this new node
212                #    - we replace the opening statement by the <py:for> node
213                #    - we delete the closing statement
214
215                if c_dir is not None:
216                    # pass the closing statements
217                    continue
218                for pair in genshi_pairs:
219                    if pair[0] == r_node:
220                        break
221                opening, closing = pair
222
223                o_ancestors = list(opening.iterancestors())
224                c_ancestors = list(closing.iterancestors())
225                for n in o_ancestors:
226                    if n in c_ancestors:
227                        ancestor = n
228                        break
229
230                genshi_node = ETElement('{%s}%s' % (self.namespaces['py'],
231                                                    directive), 
232                                        attrib={attr: a_val},
233                                        nsmap=self.namespaces)
234                can_append = False
235                for node in ancestor.iterchildren():
236                    if node in o_ancestors:
237                        outermost_o_ancestor = node
238                        can_append = True
239                        continue
240                    if node in c_ancestors:
241                        outermost_c_ancestor = node
242                        break
243                    if can_append:
244                        genshi_node.append(node)
245                ancestor.replace(outermost_o_ancestor, genshi_node)
246                ancestor.remove(outermost_c_ancestor)
247            else:
248                # It's not a genshi statement it's a python expression
249                r_node.attrib[genshi_replace] = expr
250                parent = r_node.getparent().getparent()
251                if parent is None or parent.tag != table_cell_tag:
252                    continue
253                if parent.attrib.get(office_valuetype, 'string') != 'string':
254                    # The grand-parent tag is a table cell we set the
255                    # office:value attribute of this cell
256                    dico = "{'%s': %s}" % (office_name, expr)
257                    parent.attrib[attrib_name] = dico
258                    parent.attrib.pop(office_name, None)
259
260    def _handle_images(self, tree):
261        draw_name = '{%s}name' % self.namespaces['draw']
262        draw_image = '{%s}image' % self.namespaces['draw']
263        python_attrs = '{%s}attrs' % self.namespaces['py']
264        xpath_expr = "//draw:frame[starts-with(@draw:name, 'image:')]"
265        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
266            d_name = draw.attrib[draw_name]
267            attr_expr = "make_href(%s, %r)" % (d_name[7:], d_name[7:])
268            image_node = ETElement(draw_image, 
269                                   attrib={python_attrs: attr_expr},
270                                   nsmap=self.namespaces)
271            draw.replace(draw[0], image_node)
272
273    def _handle_innerdocs(self, tree):
274        href_attrib = '{%s}href' % self.namespaces['xlink']
275        xpath_expr = "//draw:object[starts-with(@xlink:href, './')" \
276                     "and @xlink:show='embed']"
277        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
278            self.inner_docs.append(draw.attrib[href_attrib][2:])
279
280    def generate(self, *args, **kwargs):
281        serializer = OOSerializer(self.filepath)
282        kwargs['make_href'] = ImageHref(serializer.outzip, kwargs)
283        generate_all = super(Template, self).generate(*args, **kwargs)
284
285        return OOStream(generate_all, serializer)
286
287
288class OOStream(genshi.core.Stream):
289
290    def __init__(self, content_stream, serializer):
291        self.events = content_stream
292        self.serializer = serializer
293
294    def render(self, method=None, encoding='utf-8', out=None, **kwargs):
295        return self.serializer(self.events)
296
297    def serialize(self, method, **kwargs):
298        return self.render(method, **kwargs)
299
300    def __or__(self, function):
301        return OOStream(self.events | function, self.serializer)
302
303
304class OOSerializer:
305
306    def __init__(self, oo_path):
307        self.inzip = zipfile.ZipFile(oo_path)
308        self.new_oo = StringIO()
309        self.outzip = zipfile.ZipFile(self.new_oo, 'w')
310        self.xml_serializer = genshi.output.XMLSerializer()
311
312    def __call__(self, stream):
313        files = {}
314        for kind, data, pos in stream:
315            if kind == genshi.core.PI and data[0] == 'relatorio':
316                stream_for = data[1]
317                continue
318            files.setdefault(stream_for, []).append((kind, data, pos))
319
320        for f in self.inzip.infolist():
321            if f.filename.startswith('ObjectReplacements'):
322                continue
323            elif f.filename in files:
324                stream = files[f.filename]
325                self.outzip.writestr(f.filename, 
326                                     _encode(self.xml_serializer(stream)))
327            else:
328                self.outzip.writestr(f, self.inzip.read(f.filename))
329        self.inzip.close()
330        self.outzip.close()
331
332        return self.new_oo
Note: See TracBrowser for help on using the browser.