1 # -*- coding: utf-8 -*-
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
6 """PDF creation library.
8 Creates one big XML from the book and its children, converts it to LaTeX
9 with TeXML, then runs it by XeLaTeX.
12 from copy import deepcopy
17 from urllib2 import urlopen
19 from lxml import etree
21 from xmlutils import Xmill, ifoption, tag_open_close
22 from librarian import DCNS, get_resource, IOFile
23 from librarian import functions
24 from pdf import PDFFormat, substitute_hyphens, fix_hanging
29 def _wrap(*args, **kw):
30 value = f(*args, **kw)
32 prefix = (u'<TeXML escape="%d">' % (1 if really else 0))
34 if isinstance(value, list):
37 if isinstance(value, tuple):
38 return prefix + value[0], value[1] + postfix
40 return prefix + value + postfix
45 def cmd(name, parms=None):
46 def wrap(self, element=None):
47 pre, post = tag_open_close('cmd', name=name)
51 e = etree.Element("parm")
53 pre += etree.tostring(e)
54 if element is not None:
56 post = "</parm>" + post
63 def mark_alien_characters(text):
64 text = re.sub(ur"([\u0400-\u04ff]+)", ur"<alien>\1</alien>", text)
68 class EduModule(Xmill):
69 def __init__(self, options=None, state=None):
70 super(EduModule, self).__init__(options, state)
71 self.activity_counter = 0
72 self.activity_last = None
73 self.exercise_counter = 0
75 def swap_endlines(txt):
76 if self.options['strofa']:
77 txt = txt.replace("/\n", '<ctrl ch="\\"/>')
79 self.register_text_filter(swap_endlines)
80 self.register_text_filter(functions.substitute_entities)
81 self.register_text_filter(mark_alien_characters)
83 def get_dc(self, element, dc_field, single=False):
84 values = map(lambda t: t.text, element.xpath("//dc:%s" % dc_field, namespaces={'dc': DCNS.uri}))
86 return values[0] if len(values) else ''
89 def handle_rdf__RDF(self, _):
90 """skip metadata in generation"""
94 def get_rightsinfo(self, element):
95 rights_lic = self.get_dc(element, 'rights.license', True)
96 return u'<cmd name="rightsinfostr">' + (u'<opt>%s</opt>' % rights_lic if rights_lic else '') + \
97 u'<parm>%s</parm>' % self.get_dc(element, 'rights', True) + \
101 def get_authors(self, element, which=None):
102 dc = self.options['wldoc'].book_info
104 authors = dc.authors_textbook + \
105 dc.authors_scenario + \
108 authors = getattr(dc, "authors_%s" % which)
109 return u', '.join(author.readable() for author in authors if author)
112 def get_title(self, element):
113 return self.get_dc(element, 'title', True)
116 def get_description(self, element):
117 desc = self.get_dc(element, 'description', single=True)
119 print '!! no description'
123 def get_curriculum(self, element):
125 for dc_tag, new in [('subject.curriculum', False), ('subject.curriculum.new', True)]:
126 identifiers = self.get_dc(element, dc_tag)
130 from curriculum.templatetags.curriculum_tags import curriculum
131 curr_elements = curriculum(identifiers)
133 curr_elements = {'identifiers': identifiers}
134 items = ['Nowa podstawa programowa:' if new else 'Podstawa programowa:']
135 newline = '<ctrl ch="\\"/>\n'
136 if 'currset' in curr_elements:
137 for (course, level), types in curr_elements['currset'].iteritems():
138 label = u'klasa' if new else u'poziom edukacyjny'
139 lines = [u'%s, %s %s' % (course, level, label)]
140 for type, currs in types.iteritems():
142 lines += [curr.title for curr in currs]
143 items.append(newline.join(lines))
146 ret.append('\n<cmd name="vspace"><parm>.6em</parm></cmd>\n'.join(
147 '<cmd name="akap"><parm>%s</parm></cmd>' % item for item in items))
148 return '\n<cmd name="vspace"><parm>1em</parm></cmd>\n'.join(ret)
150 def handle_utwor(self, element):
153 <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
155 \\documentclass[%s]{wl}
156 \\usepackage{style}''' % self.options['customization_str'],
157 self.options['has_cover'] and '\usepackage{makecover}',
158 (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
159 (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
160 (self.options['morefloats'] == 'none' and
161 u'''\\IfFileExists{morefloats.sty}{
162 \\usepackage{morefloats}
164 u'''\\def\\authors{%s}''' % self.get_authors(element),
165 u'''\\def\\authorsexpert{%s}''' % self.get_authors(element, 'expert'),
166 u'''\\def\\authorsscenario{%s}''' % self.get_authors(element, 'scenario'),
167 u'''\\def\\authorstextbook{%s}''' % self.get_authors(element, 'textbook'),
168 u'''\\def\\description{%s}''' % self.get_description(element),
170 u'''\\author{\\authors}''',
171 u'''\\title{%s}''' % self.get_title(element),
172 u'''\\def\\bookurl{%s}''' % self.options['wldoc'].book_info.url.canonical(),
173 u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
174 u'''\\def\\curriculum{%s}''' % self.get_curriculum(element),
178 return u"".join(filter(None, lines)), u'</TeXML>'
181 def handle_powiesc(self, element):
183 <env name="document">
184 <cmd name="maketitle"/>
185 """, """<cmd name="editorialsection" /></env>"""
188 def handle_texcommand(self, element):
189 cmd = functions.texcommand(element.tag)
190 return u'<TeXML escape="1"><cmd name="%s"><parm>' % cmd, u'</parm></cmd></TeXML>'
194 handle_akap_dialog = \
195 handle_autor_utworu = \
197 handle_didaskalia = \
198 handle_didask_tekst = \
199 handle_dlugi_cytat = \
200 handle_dzielo_nadrzedne = \
201 handle_lista_osoba = \
203 handle_miejsce_czas = \
205 handle_motto_podpis = \
206 handle_naglowek_akt = \
207 handle_naglowek_czesc = \
208 handle_naglowek_listy = \
209 handle_naglowek_osoba = \
210 handle_naglowek_scena = \
211 handle_nazwa_utworu = \
217 handle_poezja_cyt = \
220 handle_sekcja_asterysk = \
221 handle_sekcja_swiatlo = \
222 handle_separator_linia = \
223 handle_slowo_obce = \
225 handle_tytul_dziela = \
226 handle_wyroznienie = \
230 def handle_naglowek_rozdzial(self, element):
231 if not self.options['teacher']:
232 if element.text.startswith((u'Wiedza', u'Zadania', u'Słowniczek', u'Dla ucznia')):
233 self.state['mute'] = False
235 self.state['mute'] = True
237 return self.handle_texcommand(element)
238 handle_naglowek_rozdzial.unmuter = True
240 def handle_naglowek_podrozdzial(self, element):
241 self.activity_counter = 0
242 if not self.options['teacher']:
243 if element.text.startswith(u'Dla ucznia'):
244 self.state['mute'] = False
246 elif element.text.startswith(u'Dla nauczyciela'):
247 self.state['mute'] = True
249 elif self.state['mute']:
251 return self.handle_texcommand(element)
252 handle_naglowek_podrozdzial.unmuter = True
254 def handle_uwaga(self, _e):
257 def handle_extra(self, _e):
260 def handle_nbsp(self, _e):
261 return '<spec cat="tilde" />'
263 _handle_strofa = cmd("strofa")
265 def handle_strofa(self, element):
266 self.options = {'strofa': True}
267 return self._handle_strofa(element)
269 def handle_aktywnosc(self, element):
270 self.activity_counter += 1
273 'activity_counter': self.activity_counter,
276 submill = EduModule(self.options, self.state)
278 if element.xpath('opis'):
279 opis = submill.generate(element.xpath('opis')[0])
283 n = element.xpath('wskazowki')
285 wskazowki = submill.generate(n[0])
288 n = element.xpath('pomoce')
291 pomoce = submill.generate(n[0])
295 forma = ''.join(element.xpath('forma/text()'))
297 czas = ''.join(element.xpath('czas/text()'))
299 counter = self.activity_counter
301 if element.getnext().tag == 'aktywnosc' or (len(self.activity_last) and self.activity_last.getnext() == element):
302 counter_tex = """<cmd name="activitycounter"><parm>%(counter)d.</parm></cmd>""" % locals()
306 self.activity_last = element
309 <cmd name="noindent" />
311 <cmd name="activityinfo"><parm>
312 <cmd name="activitytime"><parm>%(czas)s</parm></cmd>
313 <cmd name="activityform"><parm>%(forma)s</parm></cmd>
314 <cmd name="activitytools"><parm>%(pomoce)s</parm></cmd>
323 handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
324 handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
326 @ifoption(sub_gen=True)
327 def handle_pomoce(self, _):
328 return "Pomoce: ", ""
330 def handle_czas(self, *_):
333 def handle_forma(self, *_):
336 def handle_lista(self, element, attrs=None):
337 ltype = element.attrib.get('typ', 'punkt')
338 if not element.findall("punkt"):
339 if ltype == 'czytelnia':
340 return 'W przygotowaniu.'
343 if ltype == 'slowniczek':
344 surl = element.attrib.get('src', None)
346 # print '** missing src on <slowniczek>, setting default'
347 surl = 'http://edukacjamedialna.edu.pl/lekcje/slowniczek/'
348 sxml = etree.fromstring(self.options['wldoc'].provider.by_uri(surl).get_string())
349 self.options = {'slowniczek': True, 'slowniczek_xml': sxml}
355 'slowniczek': 'itemize',
356 'czytelnia': 'itemize'
359 return u'<env name="%s">' % listcmd, u'</env>'
361 def handle_punkt(self, element):
362 return '<cmd name="item"/>', ''
364 def handle_cwiczenie(self, element):
365 exercise_handlers = {
367 'uporzadkuj': Uporzadkuj,
370 'przyporzadkuj': Przyporzadkuj,
371 'prawdafalsz': PrawdaFalsz
374 typ = element.attrib['typ']
375 self.exercise_counter += 1
376 if typ not in exercise_handlers:
377 return '(no handler)'
378 self.options = {'exercise_counter': self.exercise_counter}
379 handler = exercise_handlers[typ](self.options, self.state)
380 return handler.generate(element)
382 # XXX this is copied from pyhtml.py, except for return and
383 # should be refactored for no code duplication
384 def handle_definiendum(self, element):
385 nxt = element.getnext()
388 # let's pull definiens from another document
389 if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
390 sxml = self.options['slowniczek_xml']
391 assert element.text != ''
392 if "'" in (element.text or ''):
393 defloc = sxml.xpath("//definiendum[text()=\"%s\"]" % (element.text or '').strip())
395 defloc = sxml.xpath("//definiendum[text()='%s']" % (element.text or '').strip())
397 definiens = defloc[0].getnext()
398 if definiens.tag == 'definiens':
399 subgen = EduModule(self.options, self.state)
400 definiens_s = subgen.generate(definiens)
402 return u'<cmd name="textbf"><parm>', u"</parm></cmd>: " + definiens_s
404 def handle_definiens(self, element):
407 def handle_podpis(self, element):
408 return u"""<env name="figure">""", u"</env>"
410 def handle_tabela(self, element):
412 for w in element.xpath("wiersz"):
414 if max_col < len(ks):
416 self.options = {'columnts': max_col}
418 # has_frames = int(element.attrib.get("ramki", "0"))
419 # if has_frames: frames_c = "framed"
420 # else: frames_c = ""
421 # return u"""<table class="%s">""" % frames_c, u"</table>"
423 <cmd name="begin"><parm>tabular</parm><parm>%s</parm></cmd>
424 ''' % ('l' * max_col), u'''<cmd name="end"><parm>tabular</parm></cmd>'''
427 def handle_wiersz(self, element):
428 return u"", u'<ctrl ch="\\"/>'
431 def handle_kol(self, element):
432 if element.getnext() is not None:
433 return u"", u'<spec cat="align" />'
436 def handle_link(self, element):
437 if element.attrib.get('url'):
438 url = element.attrib.get('url')
439 if url == element.text:
440 return cmd('url')(self, element)
442 return cmd('href', parms=[element.attrib['url']])(self, element)
444 return cmd('emph')(self, element)
446 def handle_obraz(self, element):
447 frmt = self.options['format']
448 name = element.attrib.get('nazwa', '').strip()
449 image = frmt.get_image(name.strip())
450 name = image.get_filename().rsplit('/', 1)[-1]
451 img_path = "obraz/%s" % name.replace("_", "")
452 frmt.attachments[img_path] = image
453 return cmd("obraz", parms=[img_path])(self)
455 def handle_video(self, element):
456 url = element.attrib.get('url')
458 print '!! <video> missing url'
460 m = re.match(r'(?:https?://)?(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
462 print '!! unknown <video> url scheme:', url
465 thumb = IOFile.from_string(urlopen("http://img.youtube.com/vi/%s/0.jpg" % name).read())
466 img_path = "video/%s.jpg" % name.replace("_", "")
467 self.options['format'].attachments[img_path] = thumb
468 canon_url = "https://www.youtube.com/watch?v=%s" % name
469 return cmd("video", parms=[img_path, canon_url])(self)
472 class Exercise(EduModule):
473 def __init__(self, *args, **kw):
474 self.question_counter = 0
475 super(Exercise, self).__init__(*args, **kw)
476 self.piece_counter = None
478 handle_rozw_kom = ifoption(teacher=True)(cmd('akap'))
480 def handle_cwiczenie(self, element):
482 'exercise': element.attrib['typ'],
485 self.question_counter = 0
486 self.piece_counter = 0
488 header = etree.Element("parm")
489 header_cmd = etree.Element("cmd", name="naglowekpodrozdzial")
490 header_cmd.append(header)
491 header.text = u"Zadanie %d." % self.options['exercise_counter']
493 pre = etree.tostring(header_cmd, encoding=unicode)
495 # Add a single <pytanie> tag if it's not there
496 if not element.xpath(".//pytanie"):
497 qpre, qpost = self.handle_pytanie(element)
502 def handle_pytanie(self, element):
503 """This will handle <cwiczenie> element, when there is no <pytanie>
505 self.question_counter += 1
506 self.piece_counter = 0
508 if self.options['teacher'] and element.attrib.get('rozw'):
509 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
512 def handle_punkt(self, element):
513 pre, post = super(Exercise, self).handle_punkt(element)
514 if self.options['teacher'] and element.attrib.get('rozw'):
515 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
518 def solution_header(self):
519 par = etree.Element("cmd", name="par")
520 parm = etree.Element("parm")
521 parm.text = u"Rozwiązanie:"
523 return etree.tostring(par)
525 def explicit_solution(self):
526 if self.options['solution']:
527 par = etree.Element("cmd", name="par")
528 parm = etree.Element("parm")
529 parm.text = self.options['solution']
531 return self.solution_header() + etree.tostring(par)
534 class Wybor(Exercise):
535 def handle_cwiczenie(self, element):
536 pre, post = super(Wybor, self).handle_cwiczenie(element)
537 is_single_choice = True
538 pytania = element.xpath(".//pytanie")
542 solutions = p.xpath(".//punkt[@rozw='prawda']")
543 if len(solutions) != 1:
544 is_single_choice = False
547 self.options = {'single': is_single_choice}
550 def handle_punkt(self, element):
551 if self.options['exercise'] and element.attrib.get('rozw', None):
552 cmd = 'radio' if self.options['single'] else 'checkbox'
553 if self.options['teacher'] and element.attrib['rozw'] == 'prawda':
555 return u'<cmd name="%s"/>' % cmd, ''
557 return super(Wybor, self).handle_punkt(element)
560 class Uporzadkuj(Exercise):
561 def handle_pytanie(self, element):
562 order_items = element.xpath(".//punkt/@rozw")
563 return super(Uporzadkuj, self).handle_pytanie(element)
566 class Przyporzadkuj(Exercise):
567 def handle_lista(self, lista):
568 header = etree.Element("parm")
569 header_cmd = etree.Element("cmd", name="par")
570 header_cmd.append(header)
571 if 'nazwa' in lista.attrib:
572 header.text = u"Kategorie:"
573 elif 'cel' in lista.attrib:
574 header.text = u"Elementy do przyporządkowania:"
576 header.text = u"Lista:"
577 pre, post = super(Przyporzadkuj, self).handle_lista(lista)
578 pre = etree.tostring(header_cmd, encoding=unicode) + pre
582 class Luki(Exercise):
583 def find_pieces(self, question):
584 return question.xpath(".//luka")
586 def solution(self, piece):
587 piece = deepcopy(piece)
590 return sub.generate(piece)
592 def handle_pytanie(self, element):
593 qpre, qpost = super(Luki, self).handle_pytanie(element)
595 luki = self.find_pieces(element)
597 self.words = u"<env name='itemize'>%s</env>" % (
598 "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
602 def handle_opis(self, element):
603 return '', self.words
605 def handle_luka(self, element):
607 if self.options['teacher']:
608 piece = deepcopy(element)
611 text = sub.generate(piece)
612 luka += u" [rozwiązanie: %s]" % text
617 def find_pieces(self, question):
618 return question.xpath(".//zastap")
620 def solution(self, piece):
621 return piece.attrib.get('rozw', '')
623 def list_header(self):
624 return u"Elementy do wstawienia"
626 def handle_zastap(self, element):
627 piece = deepcopy(element)
630 text = sub.generate(piece)
631 if self.options['teacher'] and element.attrib.get('rozw'):
632 text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
636 class PrawdaFalsz(Exercise):
637 def handle_punkt(self, element):
638 pre, post = super(PrawdaFalsz, self).handle_punkt(element)
639 if 'rozw' in element.attrib:
640 post += u" [Prawda/Fałsz]"
645 lists = tree.xpath(".//lista")
662 class EduModulePDFFormat(PDFFormat):
663 style = get_resource('res/styles/edumed/pdf/edumed.sty')
666 substitute_hyphens(self.wldoc.edoc)
667 fix_hanging(self.wldoc.edoc)
669 self.attachments = {}
673 "teacher": self.customization.get('teacher'),
675 texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
677 open("/tmp/texml.xml", "w").write(texml)
680 def get_tex_dir(self):
681 temp = super(EduModulePDFFormat, self).get_tex_dir()
682 shutil.copy(get_resource('res/styles/edumed/logo.png'), temp)
683 for name, iofile in self.attachments.items():
684 iofile.save_as(os.path.join(temp, name))
687 def get_image(self, name):
688 return self.wldoc.source.attachments[name]