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):
124 identifiers = self.get_dc(element, 'subject.curriculum')
128 from curriculum.templatetags.curriculum_tags import curriculum
129 curr_elements = curriculum(identifiers)
131 curr_elements = {'identifiers': identifiers}
132 items = ['Podstawa programowa:']
133 newline = '<ctrl ch="\\"/>\n'
134 if 'currset' in curr_elements:
135 for (course, level), types in curr_elements['currset'].iteritems():
136 lines = [u'%s, %s poziom edukacyjny' % (course, level)]
137 for type, currs in types.iteritems():
139 lines += [curr.title for curr in currs]
140 items.append(newline.join(lines))
143 return '\n<cmd name="vspace"><parm>.6em</parm></cmd>\n'.join(
144 '<cmd name="akap"><parm>%s</parm></cmd>' % item for item in items)
146 def handle_utwor(self, element):
149 <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
151 \\documentclass[%s]{wl}
152 \\usepackage{style}''' % self.options['customization_str'],
153 self.options['has_cover'] and '\usepackage{makecover}',
154 (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
155 (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
156 (self.options['morefloats'] == 'none' and
157 u'''\\IfFileExists{morefloats.sty}{
158 \\usepackage{morefloats}
160 u'''\\def\\authors{%s}''' % self.get_authors(element),
161 u'''\\def\\authorsexpert{%s}''' % self.get_authors(element, 'expert'),
162 u'''\\def\\authorsscenario{%s}''' % self.get_authors(element, 'scenario'),
163 u'''\\def\\authorstextbook{%s}''' % self.get_authors(element, 'textbook'),
164 u'''\\def\\description{%s}''' % self.get_description(element),
166 u'''\\author{\\authors}''',
167 u'''\\title{%s}''' % self.get_title(element),
168 u'''\\def\\bookurl{%s}''' % self.options['wldoc'].book_info.url.canonical(),
169 u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
170 u'''\\def\\curriculum{%s}''' % self.get_curriculum(element),
174 return u"".join(filter(None, lines)), u'</TeXML>'
177 def handle_powiesc(self, element):
179 <env name="document">
180 <cmd name="maketitle"/>
181 """, """<cmd name="editorialsection" /></env>"""
184 def handle_texcommand(self, element):
185 cmd = functions.texcommand(element.tag)
186 return u'<TeXML escape="1"><cmd name="%s"><parm>' % cmd, u'</parm></cmd></TeXML>'
190 handle_akap_dialog = \
191 handle_autor_utworu = \
193 handle_didaskalia = \
194 handle_didask_tekst = \
195 handle_dlugi_cytat = \
196 handle_dzielo_nadrzedne = \
197 handle_lista_osoba = \
199 handle_miejsce_czas = \
201 handle_motto_podpis = \
202 handle_naglowek_akt = \
203 handle_naglowek_czesc = \
204 handle_naglowek_listy = \
205 handle_naglowek_osoba = \
206 handle_naglowek_scena = \
207 handle_nazwa_utworu = \
213 handle_poezja_cyt = \
216 handle_sekcja_asterysk = \
217 handle_sekcja_swiatlo = \
218 handle_separator_linia = \
219 handle_slowo_obce = \
221 handle_tytul_dziela = \
222 handle_wyroznienie = \
226 def handle_naglowek_rozdzial(self, element):
227 if not self.options['teacher']:
228 if element.text.startswith((u'Wiedza', u'Zadania', u'Słowniczek', u'Dla ucznia')):
229 self.state['mute'] = False
231 self.state['mute'] = True
233 return self.handle_texcommand(element)
234 handle_naglowek_rozdzial.unmuter = True
236 def handle_naglowek_podrozdzial(self, element):
237 self.activity_counter = 0
238 if not self.options['teacher']:
239 if element.text.startswith(u'Dla ucznia'):
240 self.state['mute'] = False
242 elif element.text.startswith(u'Dla nauczyciela'):
243 self.state['mute'] = True
245 elif self.state['mute']:
247 return self.handle_texcommand(element)
248 handle_naglowek_podrozdzial.unmuter = True
250 def handle_uwaga(self, _e):
253 def handle_extra(self, _e):
256 def handle_nbsp(self, _e):
257 return '<spec cat="tilde" />'
259 _handle_strofa = cmd("strofa")
261 def handle_strofa(self, element):
262 self.options = {'strofa': True}
263 return self._handle_strofa(element)
265 def handle_aktywnosc(self, element):
266 self.activity_counter += 1
269 'activity_counter': self.activity_counter,
272 submill = EduModule(self.options, self.state)
274 if element.xpath('opis'):
275 opis = submill.generate(element.xpath('opis')[0])
279 n = element.xpath('wskazowki')
281 wskazowki = submill.generate(n[0])
284 n = element.xpath('pomoce')
287 pomoce = submill.generate(n[0])
291 forma = ''.join(element.xpath('forma/text()'))
293 czas = ''.join(element.xpath('czas/text()'))
295 counter = self.activity_counter
297 if element.getnext().tag == 'aktywnosc' or (len(self.activity_last) and self.activity_last.getnext() == element):
298 counter_tex = """<cmd name="activitycounter"><parm>%(counter)d.</parm></cmd>""" % locals()
302 self.activity_last = element
305 <cmd name="noindent" />
307 <cmd name="activityinfo"><parm>
308 <cmd name="activitytime"><parm>%(czas)s</parm></cmd>
309 <cmd name="activityform"><parm>%(forma)s</parm></cmd>
310 <cmd name="activitytools"><parm>%(pomoce)s</parm></cmd>
319 handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
320 handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
322 @ifoption(sub_gen=True)
323 def handle_pomoce(self, _):
324 return "Pomoce: ", ""
326 def handle_czas(self, *_):
329 def handle_forma(self, *_):
332 def handle_lista(self, element, attrs=None):
333 ltype = element.attrib.get('typ', 'punkt')
334 if not element.findall("punkt"):
335 if ltype == 'czytelnia':
336 return 'W przygotowaniu.'
339 if ltype == 'slowniczek':
340 surl = element.attrib.get('src', None)
342 # print '** missing src on <slowniczek>, setting default'
343 surl = 'http://edukacjamedialna.edu.pl/lekcje/slowniczek/'
344 sxml = etree.fromstring(self.options['wldoc'].provider.by_uri(surl).get_string())
345 self.options = {'slowniczek': True, 'slowniczek_xml': sxml}
351 'slowniczek': 'itemize',
352 'czytelnia': 'itemize'
355 return u'<env name="%s">' % listcmd, u'</env>'
357 def handle_punkt(self, element):
358 return '<cmd name="item"/>', ''
360 def handle_cwiczenie(self, element):
361 exercise_handlers = {
363 'uporzadkuj': Uporzadkuj,
366 'przyporzadkuj': Przyporzadkuj,
367 'prawdafalsz': PrawdaFalsz
370 typ = element.attrib['typ']
371 self.exercise_counter += 1
372 if typ not in exercise_handlers:
373 return '(no handler)'
374 self.options = {'exercise_counter': self.exercise_counter}
375 handler = exercise_handlers[typ](self.options, self.state)
376 return handler.generate(element)
378 # XXX this is copied from pyhtml.py, except for return and
379 # should be refactored for no code duplication
380 def handle_definiendum(self, element):
381 nxt = element.getnext()
384 # let's pull definiens from another document
385 if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
386 sxml = self.options['slowniczek_xml']
387 assert element.text != ''
388 if "'" in (element.text or ''):
389 defloc = sxml.xpath("//definiendum[text()=\"%s\"]" % (element.text or '').strip())
391 defloc = sxml.xpath("//definiendum[text()='%s']" % (element.text or '').strip())
393 definiens = defloc[0].getnext()
394 if definiens.tag == 'definiens':
395 subgen = EduModule(self.options, self.state)
396 definiens_s = subgen.generate(definiens)
398 return u'<cmd name="textbf"><parm>', u"</parm></cmd>: " + definiens_s
400 def handle_definiens(self, element):
403 def handle_podpis(self, element):
404 return u"""<env name="figure">""", u"</env>"
406 def handle_tabela(self, element):
408 for w in element.xpath("wiersz"):
410 if max_col < len(ks):
412 self.options = {'columnts': max_col}
414 # has_frames = int(element.attrib.get("ramki", "0"))
415 # if has_frames: frames_c = "framed"
416 # else: frames_c = ""
417 # return u"""<table class="%s">""" % frames_c, u"</table>"
419 <cmd name="begin"><parm>tabular</parm><parm>%s</parm></cmd>
420 ''' % ('l' * max_col), u'''<cmd name="end"><parm>tabular</parm></cmd>'''
423 def handle_wiersz(self, element):
424 return u"", u'<ctrl ch="\\"/>'
427 def handle_kol(self, element):
428 if element.getnext() is not None:
429 return u"", u'<spec cat="align" />'
432 def handle_link(self, element):
433 if element.attrib.get('url'):
434 url = element.attrib.get('url')
435 if url == element.text:
436 return cmd('url')(self, element)
438 return cmd('href', parms=[element.attrib['url']])(self, element)
440 return cmd('emph')(self, element)
442 def handle_obraz(self, element):
443 frmt = self.options['format']
444 name = element.attrib.get('nazwa', '').strip()
445 image = frmt.get_image(name.strip())
446 name = image.get_filename().rsplit('/', 1)[-1]
447 img_path = "obraz/%s" % name.replace("_", "")
448 frmt.attachments[img_path] = image
449 return cmd("obraz", parms=[img_path])(self)
451 def handle_video(self, element):
452 url = element.attrib.get('url')
454 print '!! <video> missing url'
456 m = re.match(r'(?:https?://)?(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
458 print '!! unknown <video> url scheme:', url
461 thumb = IOFile.from_string(urlopen("http://img.youtube.com/vi/%s/0.jpg" % name).read())
462 img_path = "video/%s.jpg" % name.replace("_", "")
463 self.options['format'].attachments[img_path] = thumb
464 canon_url = "https://www.youtube.com/watch?v=%s" % name
465 return cmd("video", parms=[img_path, canon_url])(self)
468 class Exercise(EduModule):
469 def __init__(self, *args, **kw):
470 self.question_counter = 0
471 super(Exercise, self).__init__(*args, **kw)
472 self.piece_counter = None
474 handle_rozw_kom = ifoption(teacher=True)(cmd('akap'))
476 def handle_cwiczenie(self, element):
478 'exercise': element.attrib['typ'],
481 self.question_counter = 0
482 self.piece_counter = 0
484 header = etree.Element("parm")
485 header_cmd = etree.Element("cmd", name="naglowekpodrozdzial")
486 header_cmd.append(header)
487 header.text = u"Zadanie %d." % self.options['exercise_counter']
489 pre = etree.tostring(header_cmd, encoding=unicode)
491 # Add a single <pytanie> tag if it's not there
492 if not element.xpath(".//pytanie"):
493 qpre, qpost = self.handle_pytanie(element)
498 def handle_pytanie(self, element):
499 """This will handle <cwiczenie> element, when there is no <pytanie>
501 self.question_counter += 1
502 self.piece_counter = 0
504 if self.options['teacher'] and element.attrib.get('rozw'):
505 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
508 def handle_punkt(self, element):
509 pre, post = super(Exercise, self).handle_punkt(element)
510 if self.options['teacher'] and element.attrib.get('rozw'):
511 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
514 def solution_header(self):
515 par = etree.Element("cmd", name="par")
516 parm = etree.Element("parm")
517 parm.text = u"Rozwiązanie:"
519 return etree.tostring(par)
521 def explicit_solution(self):
522 if self.options['solution']:
523 par = etree.Element("cmd", name="par")
524 parm = etree.Element("parm")
525 parm.text = self.options['solution']
527 return self.solution_header() + etree.tostring(par)
530 class Wybor(Exercise):
531 def handle_cwiczenie(self, element):
532 pre, post = super(Wybor, self).handle_cwiczenie(element)
533 is_single_choice = True
534 pytania = element.xpath(".//pytanie")
538 solutions = p.xpath(".//punkt[@rozw='prawda']")
539 if len(solutions) != 1:
540 is_single_choice = False
543 self.options = {'single': is_single_choice}
546 def handle_punkt(self, element):
547 if self.options['exercise'] and element.attrib.get('rozw', None):
548 cmd = 'radio' if self.options['single'] else 'checkbox'
549 if self.options['teacher'] and element.attrib['rozw'] == 'prawda':
551 return u'<cmd name="%s"/>' % cmd, ''
553 return super(Wybor, self).handle_punkt(element)
556 class Uporzadkuj(Exercise):
557 def handle_pytanie(self, element):
558 order_items = element.xpath(".//punkt/@rozw")
559 return super(Uporzadkuj, self).handle_pytanie(element)
562 class Przyporzadkuj(Exercise):
563 def handle_lista(self, lista):
564 header = etree.Element("parm")
565 header_cmd = etree.Element("cmd", name="par")
566 header_cmd.append(header)
567 if 'nazwa' in lista.attrib:
568 header.text = u"Kategorie:"
569 elif 'cel' in lista.attrib:
570 header.text = u"Elementy do przyporządkowania:"
572 header.text = u"Lista:"
573 pre, post = super(Przyporzadkuj, self).handle_lista(lista)
574 pre = etree.tostring(header_cmd, encoding=unicode) + pre
578 class Luki(Exercise):
579 def find_pieces(self, question):
580 return question.xpath(".//luka")
582 def solution(self, piece):
583 piece = deepcopy(piece)
586 return sub.generate(piece)
588 def handle_pytanie(self, element):
589 qpre, qpost = super(Luki, self).handle_pytanie(element)
591 luki = self.find_pieces(element)
593 self.words = u"<env name='itemize'>%s</env>" % (
594 "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
598 def handle_opis(self, element):
599 return '', self.words
601 def handle_luka(self, element):
603 if self.options['teacher']:
604 piece = deepcopy(element)
607 text = sub.generate(piece)
608 luka += u" [rozwiązanie: %s]" % text
613 def find_pieces(self, question):
614 return question.xpath(".//zastap")
616 def solution(self, piece):
617 return piece.attrib.get('rozw', '')
619 def list_header(self):
620 return u"Elementy do wstawienia"
622 def handle_zastap(self, element):
623 piece = deepcopy(element)
626 text = sub.generate(piece)
627 if self.options['teacher'] and element.attrib.get('rozw'):
628 text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
632 class PrawdaFalsz(Exercise):
633 def handle_punkt(self, element):
634 pre, post = super(PrawdaFalsz, self).handle_punkt(element)
635 if 'rozw' in element.attrib:
636 post += u" [Prawda/Fałsz]"
641 lists = tree.xpath(".//lista")
658 class EduModulePDFFormat(PDFFormat):
659 style = get_resource('res/styles/edumed/pdf/edumed.sty')
662 substitute_hyphens(self.wldoc.edoc)
663 fix_hanging(self.wldoc.edoc)
665 self.attachments = {}
669 "teacher": self.customization.get('teacher'),
671 texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
673 open("/tmp/texml.xml", "w").write(texml)
676 def get_tex_dir(self):
677 temp = super(EduModulePDFFormat, self).get_tex_dir()
678 shutil.copy(get_resource('res/styles/edumed/logo.png'), temp)
679 for name, iofile in self.attachments.items():
680 iofile.save_as(os.path.join(temp, name))
683 def get_image(self, name):
684 return self.wldoc.source.attachments[name]