4677229000dc78906dcc7b753b8779e7ae494640
[librarian.git] / librarian / epub.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 from __future__ import with_statement
7
8 import os
9 import os.path
10 import re
11 import subprocess
12 from StringIO import StringIO
13 from copy import deepcopy
14 from mimetypes import guess_type
15
16 from lxml import etree
17 import zipfile
18 from tempfile import mkdtemp, NamedTemporaryFile
19 from shutil import rmtree
20
21 from librarian import RDFNS, WLNS, NCXNS, OPFNS, XHTMLNS, DCNS, OutputFile
22 from librarian.cover import DefaultEbookCover
23
24 from librarian import functions, get_resource
25
26 from librarian.hyphenator import Hyphenator
27
28 functions.reg_person_name()
29 functions.reg_lang_code_3to2()
30
31
32 def set_hyph_language(source_tree):
33     def get_short_lng_code(text):
34         result = ''
35         text = ''.join(text)
36         with open(get_resource('res/ISO-639-2_8859-1.txt'), 'rb') as f:
37             for line in f:
38                 list = line.strip().split('|')
39                 if list[0] == text:
40                     result = list[2]
41         if result == '':
42             return text
43         else:
44             return result
45     bibl_lng = etree.XPath('//dc:language//text()',
46                            namespaces={'dc': str(DCNS)})(source_tree)
47     short_lng = get_short_lng_code(bibl_lng[0])
48     try:
49         return Hyphenator(get_resource('res/hyph-dictionaries/hyph_' +
50                                        short_lng + '.dic'))
51     except:
52         pass
53
54
55 def hyphenate_and_fix_conjunctions(source_tree, hyph):
56     texts = etree.XPath('/utwor/*[2]//text()')(source_tree)
57     for t in texts:
58         parent = t.getparent()
59         if hyph is not None:
60             newt = ''
61             wlist = re.compile(r'\w+|[^\w]', re.UNICODE).findall(t)
62             for w in wlist:
63                 newt += hyph.inserted(w, u'\u00AD')
64         else:
65             newt = t
66         newt = re.sub(r'(?<=\s\w)\s+', u'\u00A0', newt)
67         if t.is_text:
68             parent.text = newt
69         elif t.is_tail:
70             parent.tail = newt
71
72
73 def inner_xml(node):
74     """ returns node's text and children as a string
75
76     >>> print inner_xml(etree.fromstring('<a>x<b>y</b>z</a>'))
77     x<b>y</b>z
78     """
79
80     nt = node.text if node.text is not None else ''
81     return ''.join([nt] + [etree.tostring(child) for child in node])
82
83
84 def set_inner_xml(node, text):
85     """ sets node's text and children from a string
86
87     >>> e = etree.fromstring('<a>b<b>x</b>x</a>')
88     >>> set_inner_xml(e, 'x<b>y</b>z')
89     >>> print etree.tostring(e)
90     <a>x<b>y</b>z</a>
91     """
92
93     p = etree.fromstring('<x>%s</x>' % text)
94     node.text = p.text
95     node[:] = p[:]
96
97
98 def node_name(node):
99     """ Find out a node's name
100
101     >>> print node_name(etree.fromstring('<a>X<b>Y</b>Z</a>'))
102     XYZ
103     """
104
105     tempnode = deepcopy(node)
106
107     for p in ('pe', 'pa', 'pt', 'pr', 'motyw'):
108         for e in tempnode.findall('.//%s' % p):
109             t = e.tail
110             e.clear()
111             e.tail = t
112     etree.strip_tags(tempnode, '*')
113     return tempnode.text
114
115
116 def xslt(xml, sheet):
117     if isinstance(xml, etree._Element):
118         xml = etree.ElementTree(xml)
119     with open(sheet) as xsltf:
120         return xml.xslt(etree.parse(xsltf))
121
122
123 def replace_characters(node):
124     def replace_chars(text):
125         if text is None:
126             return None
127         return text.replace(u"\ufeff", u"")\
128                    .replace("---", u"\u2014")\
129                    .replace("--", u"\u2013")\
130                    .replace(",,", u"\u201E")\
131                    .replace('"', u"\u201D")\
132                    .replace("'", u"\u2019")
133     if node.tag in ('uwaga', 'extra'):
134         t = node.tail
135         node.clear()
136         node.tail = t
137     node.text = replace_chars(node.text)
138     node.tail = replace_chars(node.tail)
139     for child in node:
140         replace_characters(child)
141
142
143 def find_annotations(annotations, source, part_no):
144     for child in source:
145         if child.tag in ('pe', 'pa', 'pt', 'pr'):
146             annotation = deepcopy(child)
147             number = str(len(annotations) + 1)
148             annotation.set('number', number)
149             annotation.set('part', str(part_no))
150             annotation.tail = ''
151             annotations.append(annotation)
152             tail = child.tail
153             child.clear()
154             child.tail = tail
155             child.text = number
156         if child.tag not in ('extra', 'uwaga'):
157             find_annotations(annotations, child, part_no)
158
159
160 class Stanza(object):
161     """
162     Converts / verse endings into verse elements in a stanza.
163
164     Slashes may only occur directly in the stanza. Any slashes in subelements
165     will be ignored, and the subelements will be put inside verse elements.
166
167     >>> s = etree.fromstring("<strofa>a <b>c</b> <b>c</b>/\\nb<x>x/\\ny</x>c/ \\nd</strofa>")
168     >>> Stanza(s).versify()
169     >>> print etree.tostring(s)
170     <strofa><wers_normalny>a <b>c</b> <b>c</b></wers_normalny><wers_normalny>b<x>x/
171     y</x>c</wers_normalny><wers_normalny>d</wers_normalny></strofa>
172
173     """
174     def __init__(self, stanza_elem):
175         self.stanza = stanza_elem
176         self.verses = []
177         self.open_verse = None
178
179     def versify(self):
180         self.push_text(self.stanza.text)
181         for elem in self.stanza:
182             self.push_elem(elem)
183             self.push_text(elem.tail)
184         tail = self.stanza.tail
185         self.stanza.clear()
186         self.stanza.tail = tail
187         self.stanza.extend(self.verses)
188
189     def open_normal_verse(self):
190         self.open_verse = self.stanza.makeelement("wers_normalny")
191         self.verses.append(self.open_verse)
192
193     def get_open_verse(self):
194         if self.open_verse is None:
195             self.open_normal_verse()
196         return self.open_verse
197
198     def push_text(self, text):
199         if not text:
200             return
201         for i, verse_text in enumerate(re.split(r"/\s*\n", text)):
202             if i:
203                 self.open_normal_verse()
204             verse = self.get_open_verse()
205             if len(verse):
206                 verse[-1].tail = (verse[-1].tail or "") + verse_text
207             else:
208                 verse.text = (verse.text or "") + verse_text
209
210     def push_elem(self, elem):
211         if elem.tag.startswith("wers"):
212             verse = deepcopy(elem)
213             verse.tail = None
214             self.verses.append(verse)
215             self.open_verse = verse
216         else:
217             appended = deepcopy(elem)
218             appended.tail = None
219             self.get_open_verse().append(appended)
220
221
222 def replace_by_verse(tree):
223     """ Find stanzas and create new verses in place of a '/' character """
224
225     stanzas = tree.findall('.//' + WLNS('strofa'))
226     for stanza in stanzas:
227         Stanza(stanza).versify()
228
229
230 def add_to_manifest(manifest, partno):
231     """ Adds a node to the manifest section in content.opf file """
232
233     partstr = 'part%d' % partno
234     e = manifest.makeelement(
235         OPFNS('item'), attrib={'id': partstr, 'href': partstr + '.html',
236                                'media-type': 'application/xhtml+xml'}
237     )
238     manifest.append(e)
239
240
241 def add_to_spine(spine, partno):
242     """ Adds a node to the spine section in content.opf file """
243
244     e = spine.makeelement(OPFNS('itemref'), attrib={'idref': 'part%d' % partno})
245     spine.append(e)
246
247
248 class TOC(object):
249     def __init__(self, name=None, part_href=None):
250         self.children = []
251         self.name = name
252         self.part_href = part_href
253         self.sub_number = None
254
255     def add(self, name, part_href, level=0, is_part=True, index=None):
256         assert level == 0 or index is None
257         if level > 0 and self.children:
258             return self.children[-1].add(name, part_href, level - 1, is_part)
259         else:
260             t = TOC(name)
261             t.part_href = part_href
262             if index is not None:
263                 self.children.insert(index, t)
264             else:
265                 self.children.append(t)
266             if not is_part:
267                 t.sub_number = len(self.children) + 1
268                 return t.sub_number
269
270     def append(self, toc):
271         self.children.append(toc)
272
273     def extend(self, toc):
274         self.children.extend(toc.children)
275
276     def depth(self):
277         if self.children:
278             return max((c.depth() for c in self.children)) + 1
279         else:
280             return 0
281
282     def href(self):
283         src = self.part_href
284         if self.sub_number is not None:
285             src += '#sub%d' % self.sub_number
286         return src
287
288     def write_to_xml(self, nav_map, counter=1):
289         for child in self.children:
290             nav_point = nav_map.makeelement(NCXNS('navPoint'))
291             nav_point.set('id', 'NavPoint-%d' % counter)
292             nav_point.set('playOrder', str(counter))
293
294             nav_label = nav_map.makeelement(NCXNS('navLabel'))
295             text = nav_map.makeelement(NCXNS('text'))
296             if child.name is not None:
297                 text.text = re.sub(r'\n', ' ', child.name)
298             else:
299                 text.text = child.name
300             nav_label.append(text)
301             nav_point.append(nav_label)
302
303             content = nav_map.makeelement(NCXNS('content'))
304             content.set('src', child.href())
305             nav_point.append(content)
306             nav_map.append(nav_point)
307             counter = child.write_to_xml(nav_point, counter + 1)
308         return counter
309
310     def html_part(self, depth=0):
311         texts = []
312         for child in self.children:
313             texts.append(
314                 "<div style='margin-left:%dem;'><a href='%s'>%s</a></div>" %
315                 (depth, child.href(), child.name))
316             texts.append(child.html_part(depth + 1))
317         return "\n".join(texts)
318
319     def html(self):
320         with open(get_resource('epub/toc.html')) as f:
321             t = unicode(f.read(), 'utf-8')
322         return t % self.html_part()
323
324
325 def used_chars(element):
326     """ Lists characters used in an ETree Element """
327     chars = set((element.text or '') + (element.tail or ''))
328     for child in element:
329         chars = chars.union(used_chars(child))
330     return chars
331
332
333 def chop(main_text):
334     """ divide main content of the XML file into chunks """
335
336     # prepare a container for each chunk
337     part_xml = etree.Element('utwor')
338     etree.SubElement(part_xml, 'master')
339     main_xml_part = part_xml[0]  # master
340
341     last_node_part = False
342
343     # the below loop are workaround for a problem with epubs in drama ebooks without acts
344     is_scene = False
345     is_act = False
346     for one_part in main_text:
347         name = one_part.tag
348         if name == 'naglowek_scena':
349             is_scene = True
350         elif name == 'naglowek_akt':
351             is_act = True
352
353     for one_part in main_text:
354         name = one_part.tag
355         if is_act is False and is_scene is True:
356             if name == 'naglowek_czesc':
357                 yield part_xml
358                 last_node_part = True
359                 main_xml_part[:] = [deepcopy(one_part)]
360             elif not last_node_part and name == "naglowek_scena":
361                 yield part_xml
362                 main_xml_part[:] = [deepcopy(one_part)]
363             else:
364                 main_xml_part.append(deepcopy(one_part))
365                 last_node_part = False
366         else:
367             if name == 'naglowek_czesc':
368                 yield part_xml
369                 last_node_part = True
370                 main_xml_part[:] = [deepcopy(one_part)]
371             elif not last_node_part and name in ("naglowek_rozdzial", "naglowek_akt", "srodtytul"):
372                 yield part_xml
373                 main_xml_part[:] = [deepcopy(one_part)]
374             else:
375                 main_xml_part.append(deepcopy(one_part))
376                 last_node_part = False
377     yield part_xml
378
379
380 def transform_chunk(chunk_xml, chunk_no, annotations, empty=False, _empty_html_static=[]):
381     """ transforms one chunk, returns a HTML string, a TOC object and a set of used characters """
382
383     toc = TOC()
384     for element in chunk_xml[0]:
385         if element.tag == "naglowek_czesc":
386             toc.add(node_name(element), "part%d.html#book-text" % chunk_no)
387         elif element.tag in ("naglowek_rozdzial", "naglowek_akt", "srodtytul"):
388             toc.add(node_name(element), "part%d.html" % chunk_no)
389         elif element.tag in ('naglowek_podrozdzial', 'naglowek_scena'):
390             subnumber = toc.add(node_name(element), "part%d.html" % chunk_no, level=1, is_part=False)
391             element.set('sub', str(subnumber))
392     if empty:
393         if not _empty_html_static:
394             _empty_html_static.append(open(get_resource('epub/emptyChunk.html')).read())
395         chars = set()
396         output_html = _empty_html_static[0]
397     else:
398         find_annotations(annotations, chunk_xml, chunk_no)
399         replace_by_verse(chunk_xml)
400         html_tree = xslt(chunk_xml, get_resource('epub/xsltScheme.xsl'))
401         chars = used_chars(html_tree.getroot())
402         output_html = etree.tostring(
403             html_tree, pretty_print=True, xml_declaration=True,
404             encoding="utf-8",
405             doctype='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ' +
406                     '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
407         )
408     return output_html, toc, chars
409
410
411 def transform(wldoc, verbose=False, style=None, html_toc=False,
412               sample=None, cover=None, flags=None, hyphenate=False, ilustr_path=''):
413     """ produces a EPUB file
414
415     sample=n: generate sample e-book (with at least n paragraphs)
416     cover: a cover.Cover factory or True for default
417     flags: less-advertising, without-fonts, working-copy
418     """
419
420     def transform_file(wldoc, chunk_counter=1, first=True, sample=None):
421         """ processes one input file and proceeds to its children """
422
423         replace_characters(wldoc.edoc.getroot())
424
425         hyphenator = set_hyph_language(wldoc.edoc.getroot()) if hyphenate else None
426         hyphenate_and_fix_conjunctions(wldoc.edoc.getroot(), hyphenator)
427
428         # every input file will have a TOC entry,
429         # pointing to starting chunk
430         toc = TOC(wldoc.book_info.title, "part%d.html" % chunk_counter)
431         chars = set()
432         if first:
433             # write book title page
434             html_tree = xslt(wldoc.edoc, get_resource('epub/xsltTitle.xsl'))
435             chars = used_chars(html_tree.getroot())
436             zip.writestr(
437                 'OPS/title.html',
438                 etree.tostring(
439                     html_tree, pretty_print=True, xml_declaration=True,
440                     encoding="utf-8",
441                     doctype='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"' +
442                             ' "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
443                 )
444             )
445             # add a title page TOC entry
446             toc.add(u"Strona tytułowa", "title.html")
447         elif wldoc.book_info.parts:
448             # write title page for every parent
449             if sample is not None and sample <= 0:
450                 chars = set()
451                 html_string = open(get_resource('epub/emptyChunk.html')).read()
452             else:
453                 html_tree = xslt(wldoc.edoc, get_resource('epub/xsltChunkTitle.xsl'))
454                 chars = used_chars(html_tree.getroot())
455                 html_string = etree.tostring(
456                     html_tree, pretty_print=True, xml_declaration=True,
457                     encoding="utf-8",
458                     doctype='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"' +
459                             ' "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
460                 )
461             zip.writestr('OPS/part%d.html' % chunk_counter, html_string)
462             add_to_manifest(manifest, chunk_counter)
463             add_to_spine(spine, chunk_counter)
464             chunk_counter += 1
465
466         if len(wldoc.edoc.getroot()) > 1:
467             # rdf before style master
468             main_text = wldoc.edoc.getroot()[1]
469         else:
470             # rdf in style master
471             main_text = wldoc.edoc.getroot()[0]
472             if main_text.tag == RDFNS('RDF'):
473                 main_text = None
474
475         if main_text is not None:
476             for chunk_xml in chop(main_text):
477                 empty = False
478                 if sample is not None:
479                     if sample <= 0:
480                         empty = True
481                     else:
482                         sample -= len(chunk_xml.xpath('//strofa|//akap|//akap_cd|//akap_dialog'))
483                 chunk_html, chunk_toc, chunk_chars = transform_chunk(chunk_xml, chunk_counter, annotations, empty)
484
485                 toc.extend(chunk_toc)
486                 chars = chars.union(chunk_chars)
487                 zip.writestr('OPS/part%d.html' % chunk_counter, chunk_html)
488                 add_to_manifest(manifest, chunk_counter)
489                 add_to_spine(spine, chunk_counter)
490                 chunk_counter += 1
491
492         for child in wldoc.parts():
493             child_toc, chunk_counter, chunk_chars, sample = transform_file(
494                 child, chunk_counter, first=False, sample=sample)
495             toc.append(child_toc)
496             chars = chars.union(chunk_chars)
497
498         return toc, chunk_counter, chars, sample
499
500     document = deepcopy(wldoc)
501     del wldoc
502
503     if flags:
504         for flag in flags:
505             document.edoc.getroot().set(flag, 'yes')
506
507     document.clean_ed_note()
508     document.clean_ed_note('abstrakt')
509
510     # add editors info
511     editors = document.editors()
512     if editors:
513         document.edoc.getroot().set('editors', u', '.join(sorted(
514             editor.readable() for editor in editors)))
515     if document.book_info.funders:
516         document.edoc.getroot().set('funders', u', '.join(
517             document.book_info.funders))
518     if document.book_info.thanks:
519         document.edoc.getroot().set('thanks', document.book_info.thanks)
520
521     opf = xslt(document.book_info.to_etree(), get_resource('epub/xsltContent.xsl'))
522     manifest = opf.find('.//' + OPFNS('manifest'))
523     guide = opf.find('.//' + OPFNS('guide'))
524     spine = opf.find('.//' + OPFNS('spine'))
525
526     output_file = NamedTemporaryFile(prefix='librarian', suffix='.epub', delete=False)
527     zip = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED)
528
529     functions.reg_mathml_epub(zip)
530
531     for i, filename in enumerate(os.listdir(ilustr_path)):
532         file_path = os.path.join(ilustr_path, filename)
533         zip.write(file_path, os.path.join('OPS', filename))
534         image_id = 'image%s' % i
535         manifest.append(etree.fromstring(
536             '<item id="%s" href="%s" media-type="%s" />' % (image_id, filename, guess_type(file_path)[0])))
537
538     # write static elements
539     mime = zipfile.ZipInfo()
540     mime.filename = 'mimetype'
541     mime.compress_type = zipfile.ZIP_STORED
542     mime.extra = ''
543     zip.writestr(mime, 'application/epub+zip')
544     zip.writestr(
545         'META-INF/container.xml',
546         '<?xml version="1.0" ?>'
547         '<container version="1.0" '
548         'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">'
549         '<rootfiles><rootfile full-path="OPS/content.opf" '
550         'media-type="application/oebps-package+xml" />'
551         '</rootfiles></container>'
552     )
553     zip.write(get_resource('res/wl-logo-small.png'),
554               os.path.join('OPS', 'logo_wolnelektury.png'))
555     zip.write(get_resource('res/jedenprocent.png'),
556               os.path.join('OPS', 'jedenprocent.png'))
557     if not style:
558         style = get_resource('epub/style.css')
559     zip.write(style, os.path.join('OPS', 'style.css'))
560
561     if cover:
562         if cover is True:
563             cover = DefaultEbookCover
564
565         cover_file = StringIO()
566         bound_cover = cover(document.book_info)
567         bound_cover.save(cover_file)
568         cover_name = 'cover.%s' % bound_cover.ext()
569         zip.writestr(os.path.join('OPS', cover_name), cover_file.getvalue())
570         del cover_file
571
572         cover_tree = etree.parse(get_resource('epub/cover.html'))
573         cover_tree.find('//' + XHTMLNS('img')).set('src', cover_name)
574         zip.writestr('OPS/cover.html', etree.tostring(
575             cover_tree, pretty_print=True, xml_declaration=True,
576             encoding="utf-8",
577             doctype='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ' +
578                     '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
579         ))
580
581         if bound_cover.uses_dc_cover:
582             if document.book_info.cover_by:
583                 document.edoc.getroot().set('data-cover-by', document.book_info.cover_by)
584             if document.book_info.cover_source:
585                 document.edoc.getroot().set('data-cover-source', document.book_info.cover_source)
586
587         manifest.append(etree.fromstring(
588             '<item id="cover" href="cover.html" media-type="application/xhtml+xml" />'))
589         manifest.append(etree.fromstring(
590             '<item id="cover-image" href="%s" media-type="%s" />' % (cover_name, bound_cover.mime_type())))
591         spine.insert(0, etree.fromstring('<itemref idref="cover"/>'))
592         opf.getroot()[0].append(etree.fromstring('<meta name="cover" content="cover-image"/>'))
593         guide.append(etree.fromstring('<reference href="cover.html" type="cover" title="Okładka"/>'))
594
595     annotations = etree.Element('annotations')
596
597     toc_file = etree.fromstring(
598         '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE ncx PUBLIC '
599         '"-//NISO//DTD ncx 2005-1//EN" '
600         '"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">'
601         '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" xml:lang="pl" '
602         'version="2005-1"><head></head><docTitle></docTitle><navMap>'
603         '</navMap></ncx>'
604     )
605     nav_map = toc_file[-1]
606
607     if html_toc:
608         manifest.append(etree.fromstring(
609             '<item id="html_toc" href="toc.html" media-type="application/xhtml+xml" />'))
610         spine.append(etree.fromstring(
611             '<itemref idref="html_toc" />'))
612         guide.append(etree.fromstring('<reference href="toc.html" type="toc" title="Spis treści"/>'))
613
614     toc, chunk_counter, chars, sample = transform_file(document, sample=sample)
615
616     if len(toc.children) < 2:
617         toc.add(u"Początek utworu", "part1.html")
618
619     # Last modifications in container files and EPUB creation
620     if len(annotations) > 0:
621         toc.add("Przypisy", "annotations.html")
622         manifest.append(etree.fromstring(
623             '<item id="annotations" href="annotations.html" media-type="application/xhtml+xml" />'))
624         spine.append(etree.fromstring(
625             '<itemref idref="annotations" />'))
626         replace_by_verse(annotations)
627         html_tree = xslt(annotations, get_resource('epub/xsltAnnotations.xsl'))
628         chars = chars.union(used_chars(html_tree.getroot()))
629         zip.writestr('OPS/annotations.html', etree.tostring(
630             html_tree, pretty_print=True, xml_declaration=True,
631             encoding="utf-8",
632             doctype='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ' +
633                     '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
634         ))
635
636     toc.add("Wesprzyj Wolne Lektury", "support.html")
637     manifest.append(etree.fromstring(
638         '<item id="support" href="support.html" media-type="application/xhtml+xml" />'))
639     spine.append(etree.fromstring(
640         '<itemref idref="support" />'))
641     html_string = open(get_resource('epub/support.html')).read()
642     chars.update(used_chars(etree.fromstring(html_string)))
643     zip.writestr('OPS/support.html', html_string)
644
645     toc.add("Strona redakcyjna", "last.html")
646     manifest.append(etree.fromstring(
647         '<item id="last" href="last.html" media-type="application/xhtml+xml" />'))
648     spine.append(etree.fromstring(
649         '<itemref idref="last" />'))
650     html_tree = xslt(document.edoc, get_resource('epub/xsltLast.xsl'))
651     chars.update(used_chars(html_tree.getroot()))
652     zip.writestr('OPS/last.html', etree.tostring(
653         html_tree, pretty_print=True, xml_declaration=True,
654         encoding="utf-8",
655         doctype='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ' +
656                 '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
657     ))
658
659     if not flags or 'without-fonts' not in flags:
660         # strip fonts
661         tmpdir = mkdtemp('-librarian-epub')
662         try:
663             cwd = os.getcwd()
664         except OSError:
665             cwd = None
666
667         os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'font-optimizer'))
668         for fname in 'DejaVuSerif.ttf', 'DejaVuSerif-Bold.ttf', 'DejaVuSerif-Italic.ttf', 'DejaVuSerif-BoldItalic.ttf':
669             optimizer_call = ['perl', 'subset.pl', '--chars',
670                               ''.join(chars).encode('utf-8'),
671                               get_resource('fonts/' + fname),
672                               os.path.join(tmpdir, fname)]
673             if verbose:
674                 print "Running font-optimizer"
675                 subprocess.check_call(optimizer_call)
676             else:
677                 subprocess.check_call(optimizer_call, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
678             zip.write(os.path.join(tmpdir, fname), os.path.join('OPS', fname))
679             manifest.append(etree.fromstring(
680                 '<item id="%s" href="%s" media-type="application/x-font-truetype" />' % (fname, fname)))
681         rmtree(tmpdir)
682         if cwd is not None:
683             os.chdir(cwd)
684     zip.writestr('OPS/content.opf', etree.tostring(opf, pretty_print=True,
685                  xml_declaration=True, encoding="utf-8"))
686     title = document.book_info.title
687     attributes = "dtb:uid", "dtb:depth", "dtb:totalPageCount", "dtb:maxPageNumber"
688     for st in attributes:
689         meta = toc_file.makeelement(NCXNS('meta'))
690         meta.set('name', st)
691         meta.set('content', '0')
692         toc_file[0].append(meta)
693     toc_file[0][0].set('content', str(document.book_info.url))
694     toc_file[0][1].set('content', str(toc.depth()))
695     set_inner_xml(toc_file[1], ''.join(('<text>', title, '</text>')))
696
697     # write TOC
698     if html_toc:
699         toc.add(u"Spis treści", "toc.html", index=1)
700         zip.writestr('OPS/toc.html', toc.html().encode('utf-8'))
701     toc.write_to_xml(nav_map)
702     zip.writestr('OPS/toc.ncx', etree.tostring(toc_file, pretty_print=True,
703                  xml_declaration=True, encoding="utf-8"))
704     zip.close()
705
706     return OutputFile.from_filename(output_file.name)