root/relatorio/templates/opendocument.py @ 41:ff123a3667db

Revision 41:ff123a3667db, 11.2 kB (checked in by Nicolas ?vrard <nicoe@…>, 5 years ago)

Support for inner documents

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 lxml.etree
31import genshi
32import genshi.output
33from genshi.template import MarkupTemplate
34
35GENSHI_TAGS = re.compile(r'''relatorio://((/)?(for|choose|otherwise|when|if|with)( (\w+)=["'](.*)["']|)|.*)''')
36EXTENSIONS = {'image/png': 'png',
37              'image/jpeg': 'jpg',
38              'image/bmp': 'bmp',
39              'image/gif': 'gif',
40              'image/tiff': 'tif',
41              'image/xbm': 'xbm',
42             }
43
44_encode = genshi.output.encode
45ETElement = lxml.etree.Element
46
47
48class ImageHref:
49   
50    def __init__(self, zipfile):
51        self.zip = zipfile
52
53    def __call__(self, expr, name):
54        bitstream, mimetype = expr
55        bitstream.seek(0)
56        file_content = bitstream.read()
57        name = md5.new(file_content).hexdigest()
58        path = 'Pictures/%s.%s' % (name, EXTENSIONS[mimetype])
59        if path not in self.zip.namelist():
60            self.zip.writestr(path, file_content)
61        return {'{http://www.w3.org/1999/xlink}href': path}
62
63
64class Template(MarkupTemplate):
65
66    def __init__(self, source, filepath=None, filename=None, loader=None,
67                 encoding=None, lookup='strict', allow_exec=True):
68        self.namespaces = {}
69        self.inner_docs = []
70        super(Template, self).__init__(source, filepath, filename, loader,
71                                       encoding, lookup, allow_exec)
72
73    def _parse(self, source, encoding):
74        inzip = zipfile.ZipFile(self.filepath)
75        content = inzip.read('content.xml')
76        styles = inzip.read('styles.xml')
77
78        genshi_obj = super(Template, self)
79        content = genshi_obj._parse(self.add_directives(content), encoding)
80        styles = genshi_obj._parse(self.add_directives(styles), encoding)
81        content_files= [('content.xml', content)]
82        styles_files = [('styles.xml', styles)]
83
84        while self.inner_docs:
85            doc = self.inner_docs.pop()
86            c_path, s_path = doc + '/content.xml', doc + '/styles.xml'
87            content = inzip.read(c_path)
88            styles = inzip.read(s_path)
89           
90            c_parsed = genshi_obj._parse(self.add_directives(content), encoding)
91            s_parsed = genshi_obj._parse(self.add_directives(styles), encoding)
92
93            content_files.append((c_path, c_parsed))
94            styles_files.append((s_path, s_parsed))
95
96        inzip.close()
97        parsed = []
98        for fpath, fparsed in content_files + styles_files:
99            parsed.append((genshi.core.PI, ('relatorio', fpath), None))
100            parsed += fparsed
101
102        return parsed
103
104    def add_directives(self, content):
105        tree = lxml.etree.parse(StringIO(content))
106        root = tree.getroot()
107        self.namespaces = root.nsmap.copy()
108        self.namespaces['py'] = 'http://genshi.edgewall.org/'
109
110        self._handle_text_a(tree)
111        self._handle_images(tree)
112        self._handle_innerdocs(tree)
113        return StringIO(lxml.etree.tostring(tree))
114
115    def _handle_text_a(self, tree):
116        """
117        Will treat all text:a tag (py:if/for/choose/when/otherwise)
118        tags
119        """
120        # Some tag name constants
121        table_cell_tag = '{%s}table-cell' % self.namespaces['table']
122        attrib_name = '{%s}attrs' % self.namespaces['py']
123        office_name = '{%s}value' % self.namespaces['office']
124        office_valuetype = '{%s}value-type' % self.namespaces['office']
125        genshi_name = '{%s}replace' % self.namespaces['py']
126        xlink_href_attrib = '{%s}href' % self.namespaces['xlink']
127
128        # First we create the list of all the text:a nodes.
129        # If this node href matches the relatorio URL it is kept.
130        # If this node href matches a genshi directive it is kept for further
131        # processing.
132        genshi_directives, text_a = [], []
133        for statement in tree.xpath('//text:a', namespaces=self.namespaces):
134            href = urllib.unquote(statement.attrib[xlink_href_attrib])
135            match_obj = GENSHI_TAGS.match(href)
136            if match_obj is None:
137                continue
138            expr, closing, directive, _, attr, attr_val = match_obj.groups()
139            if directive is not None:
140                genshi_directives.append((statement, href))
141            text_a.append((statement, 
142                           (expr, closing, directive, attr, attr_val)))
143
144        # Then we match the opening and closing directives together
145        idx = 0
146        genshi_pairs, inserted = [], []
147        for statement, href in genshi_directives:
148            if not href.startswith('relatorio:///'):
149                genshi_pairs.append([statement, None])
150                inserted.append(idx)
151                idx += 1
152            else:
153                genshi_pairs[inserted.pop()][1] = statement
154
155        for a_node, parsed in text_a:
156            expr, c_dir, directive, attr, a_val = parsed
157
158            if directive is not None:
159                # If the text:a is a genshi directive statement:
160                #    - we operate only on opening statement
161                #    - we find the nearest ancestor of the closing and opening
162                #      statement
163                #    - we create a <py:xxx> node
164                #    - we add all the node between the opening and closing
165                #      statements to this new node
166                #    - we replace the opening statement by the <py:for> node
167                #    - we delete the closing statement
168
169                if c_dir is not None:
170                    # pass the closing statements
171                    continue
172                for pair in genshi_pairs:
173                    if pair[0] == a_node:
174                        break
175                opening, closing = pair
176
177                o_ancestors = list(opening.iterancestors())
178                c_ancestors = list(closing.iterancestors())
179                for n in o_ancestors:
180                    if n in c_ancestors:
181                        ancestor = n
182                        break
183
184                genshi_node = ETElement('{%s}%s' % (self.namespaces['py'],
185                                                    directive), 
186                                        attrib={attr: a_val},
187                                        nsmap=self.namespaces)
188                can_append = False
189                for node in ancestor.iterchildren():
190                    if node in o_ancestors:
191                        outermost_o_ancestor = node
192                        can_append = True
193                        continue
194                    if node in c_ancestors:
195                        outermost_c_ancestor = node
196                        break
197                    if can_append:
198                        genshi_node.append(node)
199                ancestor.replace(outermost_o_ancestor, genshi_node)
200                ancestor.remove(outermost_c_ancestor)
201            else:
202                # It's not a genshi statement it's a python expression
203                a_node.attrib['{%s}replace' % self.namespaces['py']] = expr
204                parent = a_node.getparent().getparent()
205                if parent is None or parent.tag != table_cell_tag:
206                    continue
207                if parent.attrib.get(office_valuetype, 'string') != 'string':
208                    # The grand-parent tag is a table cell we set the
209                    # office:value attribute of this cell
210                    dico = "{'%s': %s}" % (office_name, expr)
211                    parent.attrib[attrib_name] = dico
212                    parent.attrib.pop(office_name, None)
213
214    def _handle_images(self, tree):
215        for draw in tree.xpath('//draw:frame', namespaces=self.namespaces):
216            d_name = draw.attrib.get('{%s}name' % self.namespaces['draw'],
217                                     '')
218            if d_name.startswith('image: '):
219                attr_expr = "make_href(%s, %r)" % (d_name[7:], d_name[7:])
220                attributes = {}
221                attributes['{%s}attrs' % self.namespaces['py']] = attr_expr
222                image_node = ETElement('{%s}image' % self.namespaces['draw'],
223                                       attrib=attributes,
224                                       nsmap=self.namespaces)
225                draw.replace(draw[0], image_node)
226
227    def _handle_innerdocs(self, tree):
228        href_attrib = '{%s}href' % self.namespaces['xlink']
229        show_attrib = '{%s}show' % self.namespaces['xlink']
230        for draw in tree.xpath('//draw:object', namespaces=self.namespaces):
231            href = draw.attrib.get(href_attrib, '')
232            show = draw.attrib.get(show_attrib, '')
233            if href.startswith('./') and show == 'embed':
234                self.inner_docs.append(href[2:])
235
236    def generate(self, *args, **kwargs):
237        serializer = OOSerializer(self.filepath)
238        kwargs['make_href'] = ImageHref(serializer.outzip)
239        generate_all = super(Template, self).generate(*args, **kwargs)
240
241        return OOStream(generate_all, serializer)
242
243
244class OOStream(genshi.core.Stream):
245
246    def __init__(self, content_stream, serializer):
247        self.events = content_stream
248        self.serializer = serializer
249
250    def render(self, method=None, encoding='utf-8', out=None, **kwargs):
251        return self.serializer(self.events)
252
253    def serialize(self, method, **kwargs):
254        return self.render(method, **kwargs)
255
256    def __or__(self, function):
257        return OOStream(self.events | function, self.serializer)
258
259
260class OOSerializer:
261
262    def __init__(self, oo_path):
263        self.inzip = zipfile.ZipFile(oo_path)
264        self.new_oo = StringIO()
265        self.outzip = zipfile.ZipFile(self.new_oo, 'w')
266        self.xml_serializer = genshi.output.XMLSerializer()
267
268    def __call__(self, stream):
269        files = {}
270        for kind, data, pos in stream:
271            if kind == genshi.core.PI and data[0] == 'relatorio':
272                stream_for = data[1]
273                continue
274            files.setdefault(stream_for, []).append((kind, data, pos))
275
276        for f in self.inzip.infolist():
277            if f.filename.startswith('ObjectReplacements'):
278                continue
279            elif f.filename in files:
280                stream = files[f.filename]
281                self.outzip.writestr(f.filename, 
282                                     _encode(self.xml_serializer(stream)))
283            else:
284                self.outzip.writestr(f, self.inzip.read(f.filename))
285        self.inzip.close()
286        self.outzip.close()
287
288        return self.new_oo
Note: See TracBrowser for help on using the browser.