Basic PDF support.
[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 from copy import deepcopy
14 import os
15 import os.path
16 import shutil
17 from StringIO import StringIO
18 from tempfile import mkdtemp, NamedTemporaryFile
19 import re
20 import random
21 from copy import deepcopy
22 from subprocess import call, PIPE
23
24 from Texml.processor import process
25 from lxml import etree
26 from lxml.etree import XMLSyntaxError, XSLTApplyError
27
28 from xmlutils import Xmill, tag, tagged, ifoption, tag_open_close
29 from librarian.dcparser import Person
30 from librarian.parser import WLDocument
31 from librarian import ParseError, DCNS, get_resource, IOFile, Format
32 from librarian import functions
33 from pdf import PDFFormat
34
35
36
37 def escape(really):
38     def deco(f):
39         def _wrap(*args, **kw):
40             value = f(*args, **kw)
41
42             prefix = (u'<TeXML escape="%d">' % (really and 1 or 0))
43             postfix = u'</TeXML>'
44             if isinstance(value, list):
45                 import pdb; pdb.set_trace()
46             if isinstance(value, tuple):
47                 return prefix + value[0], value[1] + postfix
48             else:
49                 return prefix + value + postfix
50         return _wrap
51     return deco
52
53
54 def cmd(name, parms=None):
55     def wrap(self, element):
56         pre, post = tag_open_close('cmd', name=name)
57
58         if parms:
59             for parm in parms:
60                 e = etree.Element("parm")
61                 e.text = parm
62                 pre += etree.tostring(e)
63         pre += "<parm>"
64         post = "</parm>" + post
65         return pre, post
66     return wrap
67
68
69 def mark_alien_characters(text):
70     text = re.sub(ur"([\u0400-\u04ff]+)", ur"<alien>\1</alien>", text)
71     return text
72
73
74 class EduModule(Xmill):
75     def __init__(self, options=None):
76         super(EduModule, self).__init__(options)
77         self.activity_counter = 0
78         self.exercise_counter = 0
79
80         def swap_endlines(txt):
81             if self.options['strofa']:
82                 txt = txt.replace("/\n", '<ctrl ch="\\"/>')
83             return txt
84         self.register_text_filter(functions.substitute_entities)
85         self.register_text_filter(mark_alien_characters)
86         self.register_text_filter(swap_endlines)
87
88     def get_dc(self, element, dc_field, single=False):
89         values = map(lambda t: t.text, element.xpath("//dc:%s" % dc_field, namespaces={'dc': DCNS.uri}))
90         if single:
91             return values[0]
92         return values
93
94     def handle_rdf__RDF(self, _):
95         "skip metadata in generation"
96         return
97
98     @escape(True)
99     def get_rightsinfo(self, element):
100         rights_lic = self.get_dc(element, 'rights.license', True)
101         return u'<cmd name="rightsinfostr">' + \
102           (rights_lic and u'<opt>%s</opt>' % rights_lic or '') +\
103           u'<parm>%s</parm>' % self.get_dc(element, 'rights', True) +\
104           u'</cmd>'
105
106     @escape(True)
107     def get_authors(self, element):
108         authors = self.get_dc(element, 'creator.expert') + \
109           self.get_dc(element, 'creator.scenario') + \
110           self.get_dc(element, 'creator.textbook')
111         return u', '.join(authors)
112
113     @escape(1)
114     def get_title(self, element):
115         return self.get_dc(element, 'title', True)
116
117     def handle_utwor(self, element):
118         lines = [
119             u'''
120     <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
121         <TeXML escape="0">
122         \\documentclass[%s]{wl}
123         \\usepackage{style}''' % self.options['customization_str'],
124     self.options['has_cover'] and '\usepackage{makecover}',
125     (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
126     (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
127     (self.options['morefloats'] == 'none' and
128      u'''\\IfFileExists{morefloats.sty}{
129             \\usepackage{morefloats}
130         }{}'''),
131     u'''\\def\\authors{%s}''' % self.get_authors(element),
132     u'''\\author{\\authors}''',
133     u'''\\title{%s}''' % self.get_title(element),
134     u'''\\def\\bookurl{%s}''' % self.get_dc(element, 'identifier.url', True),
135     u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
136     u'</TeXML>']
137
138         return u"".join(filter(None, lines)), u'</TeXML>'
139
140
141     @escape(1)
142     def handle_powiesc(self, element):
143         return u"""
144     <env name="document">
145     <cmd name="maketitle"/>
146     """, """</env>"""
147
148     @escape(1)
149     def handle_texcommand(self, element):
150         cmd = functions.texcommand(element.tag)
151         return u'<TeXML escape="1"><cmd name="%s"><parm>' % cmd, u'</parm></cmd></TeXML>'
152
153     handle_akap = \
154     handle_akap = \
155     handle_akap_cd = \
156     handle_akap_cd = \
157     handle_akap_dialog = \
158     handle_akap_dialog = \
159     handle_autor_utworu = \
160     handle_dedykacja = \
161     handle_didaskalia = \
162     handle_didask_tekst = \
163     handle_dlugi_cytat = \
164     handle_dzielo_nadrzedne = \
165     handle_lista_osoba = \
166     handle_mat = \
167     handle_miejsce_czas = \
168     handle_motto = \
169     handle_motto_podpis = \
170     handle_naglowek_akt = \
171     handle_naglowek_czesc = \
172     handle_naglowek_listy = \
173     handle_naglowek_osoba = \
174     handle_naglowek_podrozdzial = \
175     handle_naglowek_podrozdzial = \
176     handle_naglowek_rozdzial = \
177     handle_naglowek_rozdzial = \
178     handle_naglowek_scena = \
179     handle_nazwa_utworu = \
180     handle_nota = \
181     handle_osoba = \
182     handle_pa = \
183     handle_pe = \
184     handle_podtytul = \
185     handle_poezja_cyt = \
186     handle_pr = \
187     handle_pt = \
188     handle_sekcja_asterysk = \
189     handle_sekcja_swiatlo = \
190     handle_separator_linia = \
191     handle_slowo_obce = \
192     handle_srodtytul = \
193     handle_tytul_dziela = \
194     handle_wyroznienie = \
195     handle_texcommand
196
197     _handle_strofa = cmd("strofa")
198
199     def handle_strofa(self, element):
200         self.options = {'strofa': True}
201         return self._handle_strofa(element)
202
203     def handle_aktywnosc(self, element):
204         self.activity_counter += 1
205         self.options = {
206             'activity': True,
207             'activity_counter': self.activity_counter,
208             'sub_gen': True,
209         }
210         submill = EduModule(self.options)
211
212         opis = submill.generate(element.xpath('opis')[0])
213
214         n = element.xpath('wskazowki')
215         if n: wskazowki = submill.generate(n[0])
216
217         else: wskazowki = ''
218         n = element.xpath('pomoce')
219
220         if n: pomoce = submill.generate(n[0])
221         else: pomoce = ''
222
223         forma = ''.join(element.xpath('forma/text()'))
224
225         czas = ''.join(element.xpath('czas/text()'))
226
227         counter = self.activity_counter
228
229         return u"""
230
231 <cmd name="activitycounter"><parm>%(counter)d.</parm></cmd>
232 <cmd name="activityinfo"><parm>
233  <cmd name="activitytime"><parm>%(czas)s</parm></cmd>
234  <cmd name="activityform"><parm>%(forma)s</parm></cmd>
235  <cmd name="activitytools"><parm>%(pomoce)s</parm></cmd>
236 </parm></cmd>
237
238
239 %(opis)s
240
241 %(wskazowki)s
242 """ % locals()
243
244     handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
245     handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
246
247     @ifoption(sub_gen=True)
248     def handle_pomoce(self, _):
249         return "Pomoce: ", ""
250
251     def handle_czas(self, *_):
252         return
253
254     def handle_forma(self, *_):
255         return
256
257     def handle_lista(self, element, attrs={}):
258         if not element.findall("punkt"):
259             return None
260         ltype = element.attrib.get('typ', 'punkt')
261         if ltype == 'slowniczek':
262             surl = element.attrib.get('href', None)
263             sxml = None
264             if surl:
265                 sxml = etree.fromstring(self.options['provider'].by_uri(surl).get_string())
266             self.options = {'slowniczek': True, 'slowniczek_xml': sxml }
267
268         listcmd = {'num': 'enumerate',
269                'punkt': 'itemize',
270                'alfa': 'itemize',
271                'slowniczek': 'itemize',
272                'czytelnia': 'itemize'}[ltype]
273
274         return u'<env name="%s">' % listcmd, u'</env>'
275
276     def handle_punkt(self, element):
277         return '<cmd name="item"/>', ''
278
279     def handle_cwiczenie(self, element):
280         exercise_handlers = {
281             'wybor': Wybor,
282             'uporzadkuj': Uporzadkuj,
283             'luki': Luki,
284             'zastap': Zastap,
285             'przyporzadkuj': Przyporzadkuj,
286             'prawdafalsz': PrawdaFalsz
287         }
288
289         typ = element.attrib['typ']
290         self.exercise_counter += 1
291         if not typ in exercise_handlers:
292             return '(no handler)'
293         self.options = {'exercise_counter': self.exercise_counter}
294         handler = exercise_handlers[typ](self.options)
295         return handler.generate(element)
296
297     # XXX this is copied from pyhtml.py, except for return and
298     # should be refactored for no code duplication
299     def handle_definiendum(self, element):
300         nxt = element.getnext()
301         definiens_s = ''
302
303         # let's pull definiens from another document
304         if self.options['slowniczek_xml'] and (not nxt or nxt.tag != 'definiens'):
305             sxml = self.options['slowniczek_xml']
306             assert element.text != ''
307             defloc = sxml.xpath("//definiendum[text()='%s']" % element.text)
308             if defloc:
309                 definiens = defloc[0].getnext()
310                 if definiens.tag == 'definiens':
311                     subgen = EduModule(self.options)
312                     definiens_s = subgen.generate(definiens)
313
314         return u'<cmd name="textbf"><parm>', u"</parm></cmd>: " + definiens_s
315
316     def handle_definiens(self, element):
317         return u"", u""
318
319     def handle_podpis(self, element):
320         return u"""<env name="figure">""", u"</env>"
321
322     def handle_tabela(self, element):
323         max_col = 0
324         for w in element.xpath("wiersz"):
325             ks = w.xpath("kol")
326             if max_col < len(ks):
327                 max_col = len(ks)
328         self.options = {'columnts': max_col}
329         # styling:
330                 #        has_frames = int(element.attrib.get("ramki", "0"))
331                 #        if has_frames: frames_c = "framed"
332                 #        else: frames_c = ""
333                 #        return u"""<table class="%s">""" % frames_c, u"</table>"
334         return u'''
335 <cmd name="begin"><parm>tabular</parm><parm>%s</parm></cmd>
336     ''' % ('l' * max_col), \
337     u'''<cmd name="end"><parm>tabular</parm></cmd>'''
338
339     @escape(1)
340     def handle_wiersz(self, element):
341         return u"", u'<ctrl ch="\\"/>'
342
343     @escape(1)
344     def handle_kol(self, element):
345         if element.getnext() is not None:
346             return u"", u'<spec cat="align" />'
347         return u"", u""
348
349     def handle_link(self, element):
350         if element.attrib.get('url'):
351             return cmd('href', parms=[element.attrib['url']])(self, element)
352         else:
353             return cmd('em')(self, element)
354
355
356 class Exercise(EduModule):
357     def __init__(self, *args, **kw):
358         self.question_counter = 0
359         super(Exercise, self).__init__(*args, **kw)
360
361     handle_rozw_kom = ifoption(teacher=True)(cmd('akap'))
362
363     def handle_cwiczenie(self, element):
364         self.options = {
365             'exercise': element.attrib['typ'],
366             'sub_gen': True,
367         }
368         self.question_counter = 0
369         self.piece_counter = 0
370
371         header = etree.Element("parm")
372         header_cmd = etree.Element("cmd", name="naglowekpodrozdzial")
373         header_cmd.append(header)
374         header.text = u"Zadanie %d." % self.options['exercise_counter']
375
376         pre = etree.tostring(header_cmd, encoding=unicode)
377         post = u""
378         # Add a single <pytanie> tag if it's not there
379         if not element.xpath(".//pytanie"):
380             qpre, qpost = self.handle_pytanie(element)
381             pre = pre + qpre
382             post = qpost + post
383         return pre, post
384
385     def handle_pytanie(self, element):
386         """This will handle <cwiczenie> element, when there is no <pytanie>
387         """
388         self.question_counter += 1
389         self.piece_counter = 0
390         pre = post = u""
391         if self.options['teacher'] and element.attrib.get('rozw'):
392             post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
393         return pre, post
394
395     def handle_punkt(self, element):
396         pre, post = super(Exercise, self).handle_punkt(element)
397         if self.options['teacher'] and element.attrib.get('rozw'):
398             post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
399         return pre, post
400
401     def solution_header(self):
402         par = etree.Element("cmd", name="par")
403         parm = etree.Element("parm")
404         parm.text = u"Rozwiązanie:"
405         par.append(parm)
406         return etree.tostring(par)
407
408     def explicit_solution(self):
409         if self.options['solution']:
410             par = etree.Element("cmd", name="par")
411             parm = etree.Element("parm")
412             parm.text = self.options['solution']
413             par.append(parm)
414             return self.solution_header() + etree.tostring(par)
415
416
417
418 class Wybor(Exercise):
419     def handle_cwiczenie(self, element):
420         pre, post = super(Wybor, self).handle_cwiczenie(element)
421         is_single_choice = True
422         pytania = element.xpath(".//pytanie")
423         if not pytania:
424             pytania = [element]
425         for p in pytania:
426             solutions = re.split(r"[, ]+", p.attrib['rozw'])
427             if len(solutions) != 1:
428                 is_single_choice = False
429                 break
430             choices = p.xpath(".//*[@nazwa]")
431             uniq = set()
432             for n in choices: uniq.add(n.attrib['nazwa'])
433             if len(choices) != len(uniq):
434                 is_single_choice = False
435                 break
436
437         self.options = {'single': is_single_choice}
438         return pre, post
439
440     def handle_punkt(self, element):
441         if self.options['exercise'] and element.attrib.get('nazwa', None):
442             cmd = 'radio' if self.options['single'] else 'checkbox'
443             return u'<cmd name="%s"/>' % cmd, ''
444         else:
445             return super(Wybor, self).handle_punkt(element)
446
447
448 class Uporzadkuj(Exercise):
449     def handle_pytanie(self, element):
450         order_items = element.xpath(".//punkt/@rozw")
451         return super(Uporzadkuj, self).handle_pytanie(element)
452
453
454 class Przyporzadkuj(Exercise):
455     def handle_lista(self, lista):
456         header = etree.Element("parm")
457         header_cmd = etree.Element("cmd", name="par")
458         header_cmd.append(header)
459         if 'nazwa' in lista.attrib:
460             header.text = u"Kategorie:"
461         elif 'cel' in lista.attrib:
462             header.text = u"Elementy do przyporządkowania:"
463         else:
464             header.text = u"Lista:"
465         pre, post = super(Przyporzadkuj, self).handle_lista(lista)
466         pre = etree.tostring(header_cmd, encoding=unicode) + pre
467         return pre, post
468
469
470 class Luki(Exercise):
471     def find_pieces(self, question):
472         return question.xpath(".//luka")
473
474     def solution(self, piece):
475         piece = deepcopy(piece)
476         piece.tail = None
477         sub = EduModule()
478         return sub.generate(piece)
479
480     def handle_pytanie(self, element):
481         qpre, qpost = super(Luki, self).handle_pytanie(element)
482
483         luki = self.find_pieces(element)
484         random.shuffle(luki)
485         self.words = u"<env name='itemize'>%s</env>" % (
486             "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
487         )
488         return qpre, qpost
489
490     def handle_opis(self, element):
491         return '', self.words
492
493     def handle_luka(self, element):
494         luka = "_" * 10
495         if self.options['teacher']:
496             piece = deepcopy(element)
497             piece.tail = None
498             sub = EduModule()
499             text = sub.generate(piece)
500             luka += u" [rozwiązanie: %s]" % text
501         return luka
502
503
504 class Zastap(Luki):
505     def find_pieces(self, question):
506         return question.xpath(".//zastap")
507
508     def solution(self, piece):
509         return piece.attrib['rozw']
510
511     def list_header(self):
512         return u"Elementy do wstawienia"
513
514     def handle_zastap(self, element):
515         piece = deepcopy(element)
516         piece.tail = None
517         sub = EduModule()
518         text = sub.generate(piece)
519         if self.options['teacher'] and element.attrib.get('rozw'):
520             text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
521         return text
522
523
524 class PrawdaFalsz(Exercise):
525     def handle_punkt(self, element):
526         pre, post = super(PrawdaFalsz, self).handle_punkt(element)
527         if 'rozw' in element.attrib:
528             post += u" [Prawda/Fałsz]"
529         return pre, post
530
531
532
533 def fix_lists(tree):
534     lists = tree.xpath(".//lista")
535     for l in lists:
536         if l.text:
537             p = l.getprevious()
538             if p is not None:
539                 if p.tail is None: p.tail = ''
540                 p.tail += l.text
541             else:
542                 p = l.getparent()
543                 if p.text is None: p.text = ''
544                 p.text += l.text
545             l.text = ''
546     return tree
547
548
549 class EduModulePDFFormat(PDFFormat):
550     def get_texml(self):
551         edumod = EduModule({"teacher": True})
552         texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
553
554         open("/tmp/texml.xml", "w").write(texml)
555         return texml