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.
8 from copy import deepcopy
9 from mimetypes import guess_type
10 from tempfile import NamedTemporaryFile
12 from urllib2 import urlopen
14 from lxml import etree
15 from librarian import OPFNS, NCXNS, XHTMLNS, DCNS
16 from librarian import core
17 from librarian.formats import Format
18 from librarian.formats.cover.evens import EvensCover
19 from librarian.output import OutputFile
20 from librarian.renderers import Register, TreeRenderer, UnknownElement
21 from librarian.utils import Context, get_resource, extend_element
24 class EpubFormat(Format):
29 renderers = Register()
31 def __init__(self, doc, cover=None, with_fonts=True):
32 super(EpubFormat, self).__init__(doc)
33 self.with_fonts = with_fonts
37 def build(self, ctx=None):
39 def add_file(url, file_id):
40 filename = url.rsplit('/', 1)[1]
41 if url.startswith('file://'):
42 url = ctx.files_path + urllib.quote(url[7:])
43 if url.startswith('/'):
44 url = 'http://milpeer.eu' + url
45 file_content = urlopen(url).read()
46 zip.writestr(os.path.join('OPS', filename), file_content)
47 manifest.append(etree.fromstring(
48 '<item id="%s" href="%s" media-type="%s" />' % (file_id, filename, guess_type(url)[0])))
50 opf = etree.parse(get_resource('formats/epub/res/content.opf'))
51 manifest = opf.find(OPFNS('manifest'))
52 guide = opf.find(OPFNS('guide'))
53 spine = opf.find(OPFNS('spine'))
55 author = ", ". join(self.doc.meta.get(DCNS('creator')) or '')
56 title = self.doc.meta.title()
57 opf.find('.//' + DCNS('creator')).text = author
58 opf.find('.//' + DCNS('title')).text = title
60 output_file = NamedTemporaryFile(prefix='librarian', suffix='.epub', delete=False)
61 zip = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED)
63 mime = zipfile.ZipInfo()
64 mime.filename = 'mimetype'
65 mime.compress_type = zipfile.ZIP_STORED
67 zip.writestr(mime, 'application/epub+zip')
68 zip.writestr('META-INF/container.xml', '<?xml version="1.0" ?><container version="1.0" '
69 'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">'
70 '<rootfiles><rootfile full-path="OPS/content.opf" '
71 'media-type="application/oebps-package+xml" />'
72 '</rootfiles></container>')
74 toc_file = etree.fromstring('<?xml version="1.0" encoding="utf-8"?><!DOCTYPE ncx PUBLIC '
75 '"-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">'
76 '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" xml:lang="pl" '
77 'version="2005-1"><head></head><docTitle></docTitle><navMap>'
79 # nav_map = toc_file[-1]
81 if self.cover is not None:
82 # cover_image = self.doc.meta.get(DCNS('relation.coverimage.url'))[0]
83 cover = self.cover(self.doc)
85 cover_output = cover.build()
86 cover_name = 'cover.%s' % cover.format_ext
87 zip.writestr(os.path.join('OPS', cover_name), cover_output.get_string())
90 cover_tree = etree.parse(get_resource('formats/epub/res/cover.html'))
91 cover_tree.find('//' + XHTMLNS('img')).set('src', cover_name)
92 zip.writestr('OPS/cover.html', etree.tostring(
93 cover_tree, method="html", pretty_print=True))
95 if cover.uses_dc_cover:
96 if self.doc.meta.get_one('cover_by'):
97 self.doc.edoc.getroot().set('data-cover-by', self.doc.meta.get_one('cover_by'))
98 if self.doc.meta.get_one('cover_source'):
99 self.doc.edoc.getroot().set('data-cover-source', self.doc.meta.get_one('cover_source'))
101 manifest.append(etree.fromstring(
102 '<item id="cover" href="cover.html" media-type="application/xhtml+xml" />'))
103 manifest.append(etree.fromstring(
104 '<item id="cover-image" href="%s" media-type="%s" />' % (cover_name, cover.mime_type())))
105 spine.insert(0, etree.fromstring('<itemref idref="cover" linear="no" />'))
106 opf.getroot()[0].append(etree.fromstring('<meta name="cover" content="cover-image"/>'))
107 guide.append(etree.fromstring('<reference href="cover.html" type="cover" title="Okładka"/>'))
110 ctx = Context(format=self)
115 ctx.footnotes = Footnotes()
119 wrap_tmpl = etree.parse(get_resource('formats/epub/res/chapter.html'))
120 for e in self.render(self.doc.edoc.getroot(), ctx):
121 if not len(e) and not (e.text and e.text.strip()):
123 wrap = deepcopy(wrap_tmpl)
124 extend_element(wrap.find('//*[@id="book-text"]'), e)
126 partstr = 'part%d' % int(e.get('part_no'))
127 manifest.append(manifest.makeelement(OPFNS('item'), attrib={
129 'href': partstr + ".html",
130 'media-type': 'application/xhtml+xml',
132 spine.append(spine.makeelement(OPFNS('itemref'), attrib={
135 zip.writestr('OPS/%s.html' % partstr, etree.tostring(wrap, method='html'))
137 for i, url in enumerate(ctx.images):
138 add_file(url, 'image%s' % i)
140 if len(ctx.footnotes.output):
141 ctx.toc.add("Przypisy", "footnotes.html")
142 manifest.append(etree.Element(
143 OPFNS('item'), id='footnotes', href='footnotes.html',
144 **{'media-type': "application/xhtml+xml"}))
145 spine.append(etree.Element('itemref', idref='footnotes'))
146 wrap = etree.parse(get_resource('formats/epub/res/footnotes.html'))
147 extend_element(wrap.find('//*[@id="footnotes"]'), ctx.footnotes.output)
149 # chars = chars.union(used_chars(html_tree.getroot()))
150 zip.writestr('OPS/footnotes.html', etree.tostring(
151 wrap, method="html", pretty_print=True))
153 zip.writestr('OPS/content.opf', etree.tostring(opf, pretty_print=True))
154 ctx.toc.render(toc_file[-1])
155 zip.writestr('OPS/toc.ncx', etree.tostring(toc_file, pretty_print=True))
157 return OutputFile.from_filename(output_file.name)
159 def render(self, element, ctx):
160 return self.renderers.get_for(element).render(element, ctx)
165 class EpubRenderer(TreeRenderer):
166 """ Renders insides as XML in a <_/> container. """
167 def container(self, ctx):
168 root, inner = super(EpubRenderer, self).container()
169 root.set("part_no", str(ctx.part_no))
172 def render(self, element, ctx):
173 subctx = self.subcontext(element, ctx)
174 wrapper, inside = self.container(ctx)
176 extend_element(inside, self.render_text(element.text, ctx))
177 for child in element:
179 child_renderer = ctx.format.renderers.get_for(child)
180 except UnknownElement:
183 if getattr(child_renderer, 'epub_separate', False):
186 for child_part in child_renderer.render(child, subctx):
188 wrapper, inside = self.container(ctx)
190 child_parts = list(child_renderer.render(child, subctx))
191 extend_element(inside, child_parts[0])
192 if len(child_parts) > 1:
194 for child_part in child_parts[1:-1]:
196 wrapper, inside = self.container(ctx)
197 extend_element(inside, child_parts[-1])
200 extend_element(inside, self.render_text(child.tail, ctx))
204 class Footnotes(object):
207 self.output = etree.Element("_")
209 def append(self, items):
212 "a", href="part%d.html#footnote-anchor-%d" % (int(items[0].get('part_no')), self.counter),
213 id="footnote-%d" % self.counter,
214 style="float:left;margin-right:1em")
215 e.text = "[%d]" % self.counter
217 self.output.append(e)
219 extend_element(self.output, item)
220 anchor = etree.Element(
221 "a", href="footnotes.html#footnote-%d" % self.counter, id="footnote-anchor-%d" % self.counter)
222 anchor.text = "[%d]" % self.counter
227 def __init__(self, title=None, href="", root=None):
235 self.href = href.format(counter=self.root.counter)
236 self.number = self.root.counter
237 self.root.counter += 1
239 def add(self, title, href):
240 subtoc = type(self)(title, href, root=self.root)
241 self.children.append(subtoc)
244 def render(self, nav_map):
245 for child in self.children:
246 nav_point = etree.Element(NCXNS('navPoint'))
247 nav_point.set('id', 'NavPoint-%d' % child.number)
248 nav_point.set('playOrder', str(child.number))
250 nav_label = etree.Element(NCXNS('navLabel'))
251 text = etree.Element(NCXNS('text'))
252 text.text = child.title
253 nav_label.append(text)
254 nav_point.append(nav_label)
256 content = etree.Element(NCXNS('content'))
257 content.set('src', child.href)
258 nav_point.append(content)
259 nav_map.append(nav_point)
260 child.render(nav_point)
265 class AsideR(EpubRenderer):
266 def render(self, element, ctx):
267 outputs = list(super(AsideR, self).render(element, ctx))
268 anchor = ctx.footnotes.append(outputs)
269 wrapper, inside = self.text_container() # etree.Element('_', part_no=str(ctx.part_no))
270 inside.append(anchor)
272 EpubFormat.renderers.register(core.Aside, None, AsideR('div'))
275 class DivR(EpubRenderer):
276 def container(self, ctx):
277 root, inner = super(DivR, self).container(ctx)
278 if getattr(ctx, 'inline', False):
280 inner.set('style', 'display: block;')
282 EpubFormat.renderers.register(core.Div, None, DivR('div'))
285 class DivImageR(EpubRenderer):
286 def render(self, element, ctx):
287 src = element.attrib.get('src', '')
288 ctx.images.append(src)
289 src = src.rsplit('/', 1)[1]
290 return super(DivImageR, self).render(element, Context(ctx, src=src))
292 def container(self, ctx):
293 root, inner = super(DivImageR, self).container(ctx)
294 src = getattr(ctx, 'src', '')
295 inner.set('src', src)
296 # inner.set('style', 'display: block; width: 60%; margin: 3em auto')
298 EpubFormat.renderers.register(core.Div, 'img', DivImageR('img'))
301 class HeaderR(EpubRenderer):
302 def subcontext(self, element, ctx):
303 return Context(ctx, inline=True)
304 EpubFormat.renderers.register(core.Header, None, HeaderR('h1'))
307 class SectionR(EpubRenderer):
310 def render(self, element, ctx):
312 if element.getparent() is not None:
313 tocitem = ctx.toc.add(element.meta.title(), 'part%d.html' % ctx.part_no)
314 ctx = Context(ctx, toc=tocitem)
315 return super(SectionR, self).render(element, ctx)
316 EpubFormat.renderers.register(core.Section, None, SectionR())
319 class SpanR(EpubRenderer):
321 EpubFormat.renderers.register(core.Span, None, SpanR('span'))