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