cb082ca3b4b3f263b9329d78f9329e06e38468ce
[librarian.git] / librarian / pypdf.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 #
6 """PDF creation library.
7
8 Creates one big XML from the book and its children, converts it to LaTeX
9 with TeXML, then runs it by XeLaTeX.
10
11 """
12 from __future__ import with_statement
13 import os
14 import os.path
15 import shutil
16 from StringIO import StringIO
17 from tempfile import mkdtemp, NamedTemporaryFile
18 import re
19 from copy import deepcopy
20 from subprocess import call, PIPE
21
22 from Texml.processor import process
23 from lxml import etree
24 from lxml.etree import XMLSyntaxError, XSLTApplyError
25
26 from xmlutils import Xmill, tag, tagged, ifoption
27 from librarian.dcparser import Person
28 from librarian.parser import WLDocument
29 from librarian import ParseError, DCNS, get_resource, IOFile, Format
30 from librarian import functions
31 from pdf import PDFFormat
32
33
34 def escape(really):
35     def deco(f):
36         def _wrap(*args, **kw):
37             value = f(*args, **kw)
38
39             prefix = (u'<TeXML escape="%d">' % (really and 1 or 0))
40             postfix = u'</TeXML>'
41             if isinstance(value, list):
42                 import pdb; pdb.set_trace()
43             if isinstance(value, tuple):
44                 return prefix + value[0], value[1] + postfix
45             else:
46                 return prefix + value + postfix
47         return _wrap
48     return deco
49
50
51 def cmd(name, pass_text=False):
52     def wrap(self, element):
53         pre = u'<cmd name="%s">' % name
54
55         if pass_text:
56             pre += "<parm>%s</parm>" % element.text
57             return pre + '</cmd>'
58         else:
59             return pre, '</cmd>'
60     return wrap
61
62
63 def mark_alien_characters(text):
64     text = re.sub(ur"([\u0400-\u04ff]+)", ur"<alien>\1</alien>", text)
65     return text
66
67
68 class EduModule(Xmill):
69     def __init__(self, options=None):
70         super(EduModule, self).__init__(options)
71         self.activity_counter = 0
72         self.register_text_filter(functions.substitute_entities)
73         self.register_text_filter(mark_alien_characters)
74
75     def get_dc(self, element, dc_field, single=False):
76         values = map(lambda t: t.text, element.xpath("//dc:%s" % dc_field, namespaces={'dc': DCNS.uri}))
77         if single:
78             return values[0]
79         return values
80
81     def handle_rdf__RDF(self, _):
82         "skip metadata in generation"
83         return
84
85     @escape(True)
86     def get_rightsinfo(self, element):
87         rights_lic = self.get_dc(element, 'rights.license', True)
88         return u'<cmd name="rightsinfostr">' + \
89           (rights_lic and u'<opt>%s</opt>' % rights_lic or '') +\
90           u'<parm>%s</parm>' % self.get_dc(element, 'rights', True) +\
91           u'</cmd>'
92
93     @escape(True)
94     def get_authors(self, element):
95         authors = self.get_dc(element, 'creator.expert') + \
96           self.get_dc(element, 'creator.scenario') + \
97           self.get_dc(element, 'creator.textbook')
98         return u', '.join(authors)
99
100     @escape(1)
101     def get_title(self, element):
102         return self.get_dc(element, 'title', True)
103
104     def handle_utwor(self, element):
105         lines = [
106             u'''
107     <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
108         <TeXML escape="0">
109         \\documentclass[%s]{wl}
110         \\usepackage{style}''' % self.options['customization_str'],
111     self.options['has_cover'] and '\usepackage{makecover}',
112     (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
113     (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
114     (self.options['morefloats'] == 'none' and
115      u'''\\IfFileExists{morefloats.sty}{
116             \\usepackage{morefloats}
117         }{}'''),
118     u'''\\def\\authors{%s}''' % self.get_authors(element),
119     u'''\\author{\\authors}''',
120     u'''\\title{%s}''' % self.get_title(element),
121     u'''\\def\\bookurl{%s}''' % self.get_dc(element, 'identifier.url', True),
122     u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
123     u'</TeXML>']
124
125         return u"".join(filter(None, lines)), u'</TeXML>'
126
127
128     handle_naglowek_rozdzial = escape(True)(cmd("naglowekrozdzial", True))
129     handle_naglowek_podrozdzial = escape(True)(cmd("naglowekpodrozdzial", True))
130
131     @escape(1)
132     def handle_powiesc(self, element):
133         return u"""
134     <env name="document">
135     <cmd name="maketitle"/>
136     """, """</env>"""
137
138     handle_autor_utworu = cmd('autorutworu', True)
139     handle_nazwa_utworu = cmd('nazwautworu', True)
140     handle_dzielo_nadrzedne = cmd('dzielonadrzedne', True)
141     handle_podtytul = cmd('podtytul', True)
142
143     handle_akap = handle_akap_dialog = handle_akap_cd = lambda s, e: ("\n", "\n")
144     handle_strofa = lambda s, e: ("\n","\n")
145
146     def handle_aktywnosc(self, element):
147         self.activity_counter += 1
148         self.options = {
149             'activity': True,
150             'activity_counter': self.activity_counter,
151             'sub_gen': True,
152         }
153         submill = EduModule(self.options)
154
155         opis = submill.generate(element.xpath('opis')[0])
156
157         n = element.xpath('wskazowki')
158         if n: wskazowki = submill.generate(n[0])
159
160         else: wskazowki = ''
161         n = element.xpath('pomoce')
162
163         if n: pomoce = submill.generate(n[0])
164         else: pomoce = ''
165
166         forma = ''.join(element.xpath('forma/text()'))
167
168         czas = ''.join(element.xpath('czas/text()'))
169
170         counter = self.activity_counter
171
172         return u"""
173 Czas: %(czas)s min
174 Forma: %(forma)s
175 %(pomoce)s
176
177 %(counter)d. %(opis)s
178
179 %(wskazowki)s
180 """ % locals()
181
182     handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
183     handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
184
185     @ifoption(sub_gen=True)
186     def handle_pomoce(self, _):
187         return "Pomoce: ", ""
188
189     def handle_czas(self, *_):
190         return
191
192     def handle_forma(self, *_):
193         return
194
195 #     def handle_cwiczenie(self, element):
196 #         exercise_handlers = {
197 #             'wybor': Wybor,
198 #             'uporzadkuj': Uporzadkuj,
199 #             'luki': Luki,
200 #             'zastap': Zastap,
201 #             'przyporzadkuj': Przyporzadkuj,
202 #             'prawdafalsz': PrawdaFalsz
203 #             }
204
205 #         typ = element.attrib['typ']
206 #         handler = exercise_handlers[typ](self.options)
207 #         return handler.generate(element)
208
209 #     # Lists
210 #     def handle_lista(self, element, attrs={}):
211 #         ltype = element.attrib.get('typ', 'punkt')
212 #         if ltype == 'slowniczek':
213 #             surl = element.attrib.get('href', None)
214 #             sxml = None
215 #             if surl:
216 #                 sxml = etree.fromstring(self.options['provider'].by_uri(surl).get_string())
217 #             self.options = {'slowniczek': True, 'slowniczek_xml': sxml }
218 #             return '<div class="slowniczek">', '</div>'
219
220 #         listtag = {'num': 'ol',
221 #                'punkt': 'ul',
222 #                'alfa': 'ul',
223 #                'czytelnia': 'ul'}[ltype]
224
225 #         classes = attrs.get('class', '')
226 #         if classes: del attrs['class']
227
228 #         attrs_s = ' '.join(['%s="%s"' % kv for kv in attrs.items()])
229 #         if attrs_s: attrs_s = ' ' + attrs_s
230
231 #         return '<%s class="lista %s %s"%s>' % (listtag, ltype, classes, attrs_s), '</%s>' % listtag
232
233 #     def handle_punkt(self, element):
234 #         if self.options['slowniczek']:
235 #             return '<dl>', '</dl>'
236 #         else:
237 #             return '<li>', '</li>'
238
239 #     def handle_definiendum(self, element):
240 #         nxt = element.getnext()
241 #         definiens_s = ''
242
243 #         # let's pull definiens from another document
244 #         if self.options['slowniczek_xml'] and (not nxt or nxt.tag != 'definiens'):
245 #             sxml = self.options['slowniczek_xml']
246 #             assert element.text != ''
247 #             defloc = sxml.xpath("//definiendum[text()='%s']" % element.text)
248 #             if defloc:
249 #                 definiens = defloc[0].getnext()
250 #                 if definiens.tag == 'definiens':
251 #                     subgen = EduModule(self.options)
252 #                     definiens_s = subgen.generate(definiens)
253
254 #         return u"<dt>", u"</dt>" + definiens_s
255
256 #     def handle_definiens(self, element):
257 #         return u"<dd>", u"</dd>"
258
259
260 #     def handle_podpis(self, element):
261 #         return u"""<div class="caption">""", u"</div>"
262
263 #     def handle_tabela(self, element):
264 #         has_frames = int(element.attrib.get("ramki", "0"))
265 #         if has_frames: frames_c = "framed"
266 #         else: frames_c = ""
267 #         return u"""<table class="%s">""" % frames_c, u"</table>"
268
269 #     def handle_wiersz(self, element):
270 #         return u"<tr>", u"</tr>"
271
272 #     def handle_kol(self, element):
273 #         return u"<td>", u"</td>"
274
275 #     def handle_rdf__RDF(self, _):
276 #         # ustal w opcjach  rzeczy :D
277 #         return
278
279 #     def handle_link(self, element):
280 #         if 'material' in element.attrib:
281 #             formats = re.split(r"[, ]+", element.attrib['format'])
282 #             fmt_links = []
283 #             for f in formats:
284 #                 fmt_links.append(u'<a href="%s">%s</a>' % (self.options['urlmapper'].url_for_material(element.attrib['material'], f), f.upper()))
285
286 #             return u"", u' (%s)' % u' '.join(fmt_links)
287
288
289 # class Exercise(EduModule):
290 #     def __init__(self, *args, **kw):
291 #         self.question_counter = 0
292 #         super(Exercise, self).__init__(*args, **kw)
293
294 #     def handle_rozw_kom(self, element):
295 #         return u"""<div style="display:none" class="comment">""", u"""</div>"""
296
297 #     def handle_cwiczenie(self, element):
298 #         self.options = {'exercise': element.attrib['typ']}
299 #         self.question_counter = 0
300 #         self.piece_counter = 0
301
302 #         pre = u"""
303 # <div class="exercise %(typ)s" data-type="%(typ)s">
304 # <form action="#" method="POST">
305 # """ % element.attrib
306 #         post = u"""
307 # <div class="buttons">
308 # <span class="message"></span>
309 # <input type="button" class="check" value="sprawdź"/>
310 # <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
311 # <input type="button" class="solutions" value="pokaż rozwiązanie"/>
312 # <input type="button" class="reset" value="reset"/>
313 # </div>
314 # </form>
315 # </div>
316 # """
317 #         # Add a single <pytanie> tag if it's not there
318 #         if not element.xpath(".//pytanie"):
319 #             qpre, qpost = self.handle_pytanie(element)
320 #             pre = pre + qpre
321 #             post = qpost + post
322 #         return pre, post
323
324 #     def handle_pytanie(self, element):
325 #         """This will handle <cwiczenie> element, when there is no <pytanie>
326 #         """
327 #         add_class = ""
328 #         self.question_counter += 1
329 #         self.piece_counter = 0
330 #         solution = element.attrib.get('rozw', None)
331 #         if solution: solution_s = ' data-solution="%s"' % solution
332 #         else: solution_s = ''
333
334 #         handles = element.attrib.get('uchwyty', None)
335 #         if handles:
336 #             add_class += ' handles handles-%s' % handles
337 #             self.options = {'handles': handles}
338
339 #         minimum = element.attrib.get('min', None)
340 #         if minimum: minimum_s = ' data-minimum="%d"' % int(minimum)
341 #         else: minimum_s = ''
342
343 #         return '<div class="question%s" data-no="%d" %s>' %\
344 #             (add_class, self.question_counter, solution_s + minimum_s), \
345 #             "</div>"
346
347 class EduModulePDFFormat(PDFFormat):
348     def get_texml(self):
349         edumod = EduModule()
350         texml = edumod.generate(self.wldoc.edoc.getroot()).encode('utf-8')
351
352         open("/tmp/texml.xml", "w").write(texml)
353         return texml