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