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
38 return self.doc.meta.get_one(DCNS(tag))
40 def build(self, ctx=None):
42 def add_file(url, file_id):
43 filename = url.rsplit('/', 1)[1]
44 if url.startswith('file://'):
45 url = ctx.files_path + urllib.quote(url[7:])
46 if url.startswith('/'):
47 url = 'http://milpeer.eu' + url
48 file_content = urlopen(url).read()
49 zip.writestr(os.path.join('OPS', filename), file_content)
50 manifest.append(etree.fromstring(
51 '<item id="%s" href="%s" media-type="%s" />' % (file_id, filename, guess_type(url)[0])))
53 opf = etree.parse(get_resource('formats/epub/res/content.opf'))
54 manifest = opf.find(OPFNS('manifest'))
55 guide = opf.find(OPFNS('guide'))
56 spine = opf.find(OPFNS('spine'))
58 author = ", ". join(self.doc.meta.get(DCNS('creator')) or [])
59 title = self.doc.meta.title()
60 opf.find('.//' + DCNS('creator')).text = author
61 opf.find('.//' + DCNS('title')).text = title
63 output_file = NamedTemporaryFile(prefix='librarian', suffix='.epub', delete=False)
64 zip = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED)
66 mime = zipfile.ZipInfo()
67 mime.filename = 'mimetype'
68 mime.compress_type = zipfile.ZIP_STORED
70 zip.writestr(mime, 'application/epub+zip')
71 zip.writestr('META-INF/container.xml', '<?xml version="1.0" ?><container version="1.0" '
72 'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">'
73 '<rootfiles><rootfile full-path="OPS/content.opf" '
74 'media-type="application/oebps-package+xml" />'
75 '</rootfiles></container>')
77 toc_file = etree.fromstring('<?xml version="1.0" encoding="utf-8"?><!DOCTYPE ncx PUBLIC '
78 '"-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">'
79 '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" xml:lang="pl" '
80 'version="2005-1"><head></head><docTitle></docTitle><navMap>'
82 # nav_map = toc_file[-1]
84 if self.cover is not None:
85 # cover_image = self.doc.meta.get(DCNS('relation.coverimage.url'))[0]
86 cover = self.cover(self.doc)
88 cover_output = cover.build()
89 cover_name = 'cover.%s' % cover.format_ext
90 zip.writestr(os.path.join('OPS', cover_name), cover_output.get_string())
93 cover_tree = etree.parse(get_resource('formats/epub/res/cover.html'))
94 cover_tree.find('//' + XHTMLNS('img')).set('src', cover_name)
95 zip.writestr('OPS/cover.html', etree.tostring(
96 cover_tree, method="html", pretty_print=True))
98 if cover.uses_dc_cover:
99 if self.doc.meta.get_one('cover_by'):
100 self.doc.edoc.getroot().set('data-cover-by', self.doc.meta.get_one('cover_by'))
101 if self.doc.meta.get_one('cover_source'):
102 self.doc.edoc.getroot().set('data-cover-source', self.doc.meta.get_one('cover_source'))
104 manifest.append(etree.fromstring(
105 '<item id="cover" href="cover.html" media-type="application/xhtml+xml" />'))
106 manifest.append(etree.fromstring(
107 '<item id="cover-image" href="%s" media-type="%s" />' % (cover_name, cover.mime_type())))
108 spine.insert(0, etree.fromstring('<itemref idref="cover" linear="no" />'))
109 opf.getroot()[0].append(etree.fromstring('<meta name="cover" content="cover-image"/>'))
110 guide.append(etree.fromstring('<reference href="cover.html" type="cover" title="Okładka"/>'))
113 ctx = Context(format=self)
118 ctx.footnotes = Footnotes()
122 wrap_tmpl = etree.parse(get_resource('formats/epub/res/chapter.html'))
123 for e in self.render(self.doc.edoc.getroot(), ctx):
124 if not len(e) and not (e.text and e.text.strip()):
126 wrap = deepcopy(wrap_tmpl)
127 extend_element(wrap.find('//*[@id="book-text"]'), e)
129 partstr = 'part%d' % int(e.get('part_no'))
130 manifest.append(manifest.makeelement(OPFNS('item'), attrib={
132 'href': partstr + ".html",
133 'media-type': 'application/xhtml+xml',
135 spine.append(spine.makeelement(OPFNS('itemref'), attrib={
138 zip.writestr('OPS/%s.html' % partstr, etree.tostring(wrap, method='html'))
140 for i, url in enumerate(ctx.images):
141 add_file(url, 'image%s' % i)
143 if len(ctx.footnotes.output):
144 ctx.toc.add("Przypisy", "footnotes.html")
145 manifest.append(etree.Element(
146 OPFNS('item'), id='footnotes', href='footnotes.html',
147 **{'media-type': "application/xhtml+xml"}))
148 spine.append(etree.Element('itemref', idref='footnotes'))
149 wrap = etree.parse(get_resource('formats/epub/res/footnotes.html'))
150 extend_element(wrap.find('//*[@id="footnotes"]'), ctx.footnotes.output)
152 # chars = chars.union(used_chars(html_tree.getroot()))
153 zip.writestr('OPS/footnotes.html', etree.tostring(
154 wrap, method="html", pretty_print=True))
157 'Information about the resource',
158 'Publisher: %s' % self.dc('publisher'),
159 'Rights: %s' % self.dc('rights'),
160 'Intended audience: %s' % self.dc('audience'),
161 self.dc('description'),
162 'Resource prepared using MIL/PEER editing platform.',
163 'Source available at %s' % ctx.source_url,
165 footer_wrap = deepcopy(wrap_tmpl)
166 footer_body = footer_wrap.find('//*[@id="book-text"]')
167 for line in footer_text:
168 footer_line = etree.Element('p')
169 footer_line.text = line
170 footer_body.append(footer_line)
171 manifest.append(manifest.makeelement(OPFNS('item'), attrib={
173 'href': "footer.html",
174 'media-type': 'application/xhtml+xml',
176 spine.append(spine.makeelement(OPFNS('itemref'), attrib={
179 zip.writestr('OPS/footer.html', etree.tostring(footer_wrap, method='html'))
181 zip.writestr('OPS/content.opf', etree.tostring(opf, pretty_print=True))
182 ctx.toc.render(toc_file[-1])
183 zip.writestr('OPS/toc.ncx', etree.tostring(toc_file, pretty_print=True))
185 return OutputFile.from_filename(output_file.name)
187 def render(self, element, ctx):
188 return self.renderers.get_for(element).render(element, ctx)
193 class EpubRenderer(TreeRenderer):
194 """ Renders insides as XML in a <_/> container. """
195 def container(self, ctx):
196 root, inner = super(EpubRenderer, self).container()
197 root.set("part_no", str(ctx.part_no))
200 def render(self, element, ctx):
201 subctx = self.subcontext(element, ctx)
202 wrapper, inside = self.container(ctx)
204 extend_element(inside, self.render_text(element.text, ctx))
205 for child in element:
207 child_renderer = ctx.format.renderers.get_for(child)
208 except UnknownElement:
211 if getattr(child_renderer, 'epub_separate', False):
214 for child_part in child_renderer.render(child, subctx):
216 wrapper, inside = self.container(ctx)
218 child_parts = list(child_renderer.render(child, subctx))
219 extend_element(inside, child_parts[0])
220 if len(child_parts) > 1:
222 for child_part in child_parts[1:-1]:
224 wrapper, inside = self.container(ctx)
225 extend_element(inside, child_parts[-1])
228 extend_element(inside, self.render_text(child.tail, ctx))
232 class Footnotes(object):
235 self.output = etree.Element("_")
237 def append(self, items):
240 "a", href="part%d.html#footnote-anchor-%d" % (int(items[0].get('part_no')), self.counter),
241 id="footnote-%d" % self.counter,
242 style="float:left;margin-right:1em")
243 e.text = "[%d]" % self.counter
245 self.output.append(e)
247 extend_element(self.output, item)
248 anchor = etree.Element(
249 "a", href="footnotes.html#footnote-%d" % self.counter, id="footnote-anchor-%d" % self.counter)
250 anchor.text = "[%d]" % self.counter
255 def __init__(self, title=None, href="", root=None):
263 self.href = href.format(counter=self.root.counter)
264 self.number = self.root.counter
265 self.root.counter += 1
267 def add(self, title, href):
268 subtoc = type(self)(title, href, root=self.root)
269 self.children.append(subtoc)
272 def render(self, nav_map):
273 for child in self.children:
274 nav_point = etree.Element(NCXNS('navPoint'))
275 nav_point.set('id', 'NavPoint-%d' % child.number)
276 nav_point.set('playOrder', str(child.number))
278 nav_label = etree.Element(NCXNS('navLabel'))
279 text = etree.Element(NCXNS('text'))
280 text.text = child.title
281 nav_label.append(text)
282 nav_point.append(nav_label)
284 content = etree.Element(NCXNS('content'))
285 content.set('src', child.href)
286 nav_point.append(content)
287 nav_map.append(nav_point)
288 child.render(nav_point)
293 class AsideR(EpubRenderer):
294 def render(self, element, ctx):
295 outputs = list(super(AsideR, self).render(element, ctx))
296 anchor = ctx.footnotes.append(outputs)
297 wrapper, inside = self.text_container() # etree.Element('_', part_no=str(ctx.part_no))
298 inside.append(anchor)
300 EpubFormat.renderers.register(core.Aside, None, AsideR('div'))
303 class DivR(EpubRenderer):
304 def container(self, ctx):
305 root, inner = super(DivR, self).container(ctx)
306 if getattr(ctx, 'inline', False):
308 inner.set('style', 'display: block;')
310 EpubFormat.renderers.register(core.Div, None, DivR('div'))
313 class DivImageR(EpubRenderer):
314 def render(self, element, ctx):
315 src = element.attrib.get('src', '')
316 ctx.images.append(src)
317 src = src.rsplit('/', 1)[1]
318 return super(DivImageR, self).render(element, Context(ctx, src=src))
320 def container(self, ctx):
321 root, inner = super(DivImageR, self).container(ctx)
322 src = getattr(ctx, 'src', '')
323 inner.set('src', src)
324 # inner.set('style', 'display: block; width: 60%; margin: 3em auto')
326 EpubFormat.renderers.register(core.Div, 'img', DivImageR('img'))
329 class HeaderR(EpubRenderer):
330 def subcontext(self, element, ctx):
331 return Context(ctx, inline=True)
332 EpubFormat.renderers.register(core.Header, None, HeaderR('h1'))
335 class SectionR(EpubRenderer):
338 def render(self, element, ctx):
340 if element.getparent() is not None:
341 tocitem = ctx.toc.add(element.meta.title(), 'part%d.html' % ctx.part_no)
342 ctx = Context(ctx, toc=tocitem)
343 return super(SectionR, self).render(element, ctx)
344 EpubFormat.renderers.register(core.Section, None, SectionR())
347 class SpanR(EpubRenderer):
349 EpubFormat.renderers.register(core.Span, None, SpanR('span'))