root/relatorio/templates/opendocument.py @ 74:4d0cc1ed4ba2

Revision 74:4d0cc1ed4ba2, 13.2 kB (checked in by Nicolas ?vrard <nicoe@…>, 5 years ago)

Templates should return StringIO.

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