diff --git a/src/calibre/ebooks/oeb/polish/cover.py b/src/calibre/ebooks/oeb/polish/cover.py index 4a5253343c..1db4d4d685 100644 --- a/src/calibre/ebooks/oeb/polish/cover.py +++ b/src/calibre/ebooks/oeb/polish/cover.py @@ -258,7 +258,10 @@ def create_epub_cover(container, cover_path, existing_image, options=None): from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.transforms.cover import CoverManager - ext = cover_path.rpartition('.')[-1].lower() + try: + ext = cover_path.rpartition('.')[-1].lower() + except Exception: + ext = 'jpeg' cname, tname = 'cover.' + ext, 'titlepage.xhtml' recommended_folders = get_recommended_folders(container, (cname, tname)) @@ -273,8 +276,12 @@ def create_epub_cover(container, cover_path, existing_image, options=None): raster_cover_item = container.generate_item(cname, id_prefix='cover') raster_cover = container.href_to_name(raster_cover_item.get('href'), container.opf_name) - with lopen(cover_path, 'rb') as src, container.open(raster_cover, 'wb') as dest: - shutil.copyfileobj(src, dest) + with container.open(raster_cover, 'wb') as dest: + if callable(cover_path): + cover_path('write_image', dest) + else: + with lopen(cover_path, 'rb') as src: + shutil.copyfileobj(src, dest) if options is None: opts = load_defaults('epub_output') keep_aspect = opts.get('preserve_cover_aspect_ratio', False) @@ -286,19 +293,22 @@ def create_epub_cover(container, cover_path, existing_image, options=None): style = 'style="height: 100%%"' templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style) else: - width, height = 600, 800 - try: - if existing_image: - width, height = identify_data(container.raw_data(existing_image, decode=False))[:2] - else: - width, height = identify(cover_path)[:2] - except: - container.log.exception("Failed to get width and height of cover") - ar = 'xMidYMid meet' if keep_aspect else 'none' - templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar) - templ = templ.replace('__viewbox__', '0 0 %d %d'%(width, height)) - templ = templ.replace('__width__', str(width)) - templ = templ.replace('__height__', str(height)) + if callable(cover_path): + templ = CoverManager.SVG_TEMPLATE + else: + width, height = 600, 800 + try: + if existing_image: + width, height = identify_data(container.raw_data(existing_image, decode=False))[:2] + else: + width, height = identify(cover_path)[:2] + except: + container.log.exception("Failed to get width and height of cover") + ar = 'xMidYMid meet' if keep_aspect else 'none' + templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar) + templ = templ.replace('__viewbox__', '0 0 %d %d'%(width, height)) + templ = templ.replace('__width__', str(width)) + templ = templ.replace('__height__', str(height)) folder = recommended_folders[tname] if folder: tname = folder + '/' + tname @@ -412,5 +422,6 @@ def set_epub_cover(container, cover_path, report, options=None): if s is not None and s != d} if link_sub: replace_links(container, link_sub, frag_map=lambda x, y:None) + return raster_cover, titlepage diff --git a/src/calibre/srv/render_book.py b/src/calibre/srv/render_book.py index 92dfdc8135..1feb993a83 100644 --- a/src/calibre/srv/render_book.py +++ b/src/calibre/srv/render_book.py @@ -12,15 +12,18 @@ from urlparse import urlparse from cssutils import replaceUrls from lxml.etree import Comment, tostring -from calibre.ebooks.oeb.base import OEB_DOCS, escape_cdata, OEB_STYLES, rewrite_links, XPath, urlunquote, XLINK +from calibre.ebooks.oeb.base import ( + OEB_DOCS, escape_cdata, OEB_STYLES, rewrite_links, XPath, urlunquote, XLINK, XHTML) from calibre.ebooks.oeb.iterator.book import extract_book from calibre.ebooks.oeb.polish.container import Container as ContainerBase +from calibre.ebooks.oeb.polish.cover import set_epub_cover, find_cover_image from calibre.ebooks.oeb.polish.toc import get_toc from calibre.ebooks.oeb.polish.utils import guess_type from calibre.utils.short_uuid import uuid4 from calibre.utils.logging import default_log RENDER_VERSION = 1 # Also change this in read_book.ui.pyj +BLANK_JPEG = b'\xff\xd8\xff\xdb\x00C\x00\x03\x02\x02\x02\x02\x02\x03\x02\x02\x02\x03\x03\x03\x03\x04\x06\x04\x04\x04\x04\x04\x08\x06\x06\x05\x06\t\x08\n\n\t\x08\t\t\n\x0c\x0f\x0c\n\x0b\x0e\x0b\t\t\r\x11\r\x0e\x0f\x10\x10\x11\x10\n\x0c\x12\x13\x12\x10\x13\x0f\x10\x10\x10\xff\xc9\x00\x0b\x08\x00\x01\x00\x01\x01\x01\x11\x00\xff\xcc\x00\x06\x00\x10\x10\x05\xff\xda\x00\x08\x01\x01\x00\x00?\x00\xd2\xcf \xff\xd9' # noqa def encode_component(x): return x.replace(',', ',c').replace('|', ',p') @@ -53,6 +56,7 @@ class Container(ContainerBase): name == self.opf_name or mt == guess_type('a.ncx') or name.startswith('META-INF/') or name == 'mimetype' } + raster_cover_name, titlepage_name = self.create_cover_page(input_fmt.lower()) self.book_render_data = data = { 'version': RENDER_VERSION, @@ -61,10 +65,13 @@ class Container(ContainerBase): 'link_uid': uuid4(), 'book_hash': book_hash, 'is_comic': input_fmt.lower() in {'cbc', 'cbz', 'cbr', 'cb7'}, + 'raster_cover_name': raster_cover_name, + 'title_page_name': titlepage_name, } # Mark the spine as dirty since we have to ensure it is normalized for name in data['spine']: self.parsed(name), self.dirty(name) + self.inject_script(data['spine']) self.virtualized_names = set() self.virtualize_resources() def manifest_data(name): @@ -76,6 +83,37 @@ class Container(ContainerBase): with lopen(os.path.join(self.root, 'calibre-book-manifest.json'), 'wb') as f: f.write(json.dumps(self.book_render_data, ensure_ascii=False).encode('utf-8')) + def create_cover_page(self, input_fmt): + if input_fmt == 'epub': + def cover_path(action, data): + if action == 'write_image': + data.write(BLANK_JPEG) + return set_epub_cover(self, cover_path, (lambda *a: None)) + raster_cover_name = find_cover_image(self, strict=True) + if raster_cover_name is None: + item = self.generate_item(name='cover.jpeg', id_prefix='cover') + raster_cover_name = self.href_to_name(item.get('href'), self.opf_name) + with self.open(raster_cover_name, 'wb') as dest: + dest.write(BLANK_JPEG) + item = self.generate_item(name='titlepage.html', id_prefix='titlepage') + titlepage_name = self.href_to_name(item.get('href'), self.opf_name) + self.dirty(self.opf_name) + return raster_cover_name, titlepage_name + + def inject_script(self, spine): + src = 'injected-script-' + self.book_render_data['link_uid'] + for name in spine: + root = self.parsed(name) + head = tuple(root.iterchildren(XHTML('head'))) + head = head[0] if head else root.makeelement(XHTML('head')) + root.insert(0, head) + script = root.makeelement(XHTML('script')) + script.set('type', 'text/javascript') + script.set('src', src) + script.set('data-secret', 'secret-key-' + self.book_render_data['link_uid']) + head.insert(0, script) + self.dirty(name) + def virtualize_resources(self): changed = set() diff --git a/src/pyj/read_book/db.pyj b/src/pyj/read_book/db.pyj index 1d86a5b419..f82e9419da 100644 --- a/src/pyj/read_book/db.pyj +++ b/src/pyj/read_book/db.pyj @@ -82,6 +82,8 @@ class DB: 'last_read': Date(), 'metadata': metadata, 'manifest': None, + 'cover_width': None, + 'cover_height': None }) ) @@ -94,12 +96,42 @@ class DB: v'delete manifest["metadata"]' self.do_op(['books'], book, _('Failed to write to the books database'), proceed, op='put') - def store_file(self, book, name, xhr, proceed): + def store_file(self, book, name, xhr, proceed, is_cover): store_as_text = xhr.responseType is 'text' fname = file_store_name(book, name) needs_encoding = not store_as_text and not self.supports_blobs book.stored_files[fname] = {'encoded':needs_encoding, 'mimetype':book.manifest.files[name].mimetype, 'store_as_text':store_as_text} - data = xhr.response + if is_cover: + self.store_cover(book, needs_encoding, xhr.response, name, fname, proceed) + else: + self.store_file_stage2(needs_encoding, xhr.response, name, fname, proceed) + + def store_cover(self, book, needs_encoding, data, name, fname, proceed): + blob = data + if needs_encoding: + blob = Blob([data], {'type':'image/jpeg'}) + url = window.URL.createObjectURL(blob) + img = new Image() + proceeded = False + + def done(): + nonlocal proceeded + if not proceeded: + proceeded = True + window.URL.revokeObjectURL(url) + self.store_file_stage2(needs_encoding, data, name, fname, proceed) + + img.onload = def(): + book.cover_width = this.width + book.cover_height = this.height + done() + + img.onerror = def(): + print('WARNING: Failed to read dimensions of cover') + done() + img.src = url + + def store_file_stage2(self, needs_encoding, data, name, fname, proceed): if needs_encoding: data = base64encode(Uint8Array(data)) req = self.idb.transaction(['files'], 'readwrite').objectStore('files').put(data, fname) diff --git a/src/pyj/read_book/ui.pyj b/src/pyj/read_book/ui.pyj index 40a18001f7..ee0f901440 100644 --- a/src/pyj/read_book/ui.pyj +++ b/src/pyj/read_book/ui.pyj @@ -8,6 +8,7 @@ from book_list.globals import get_boss from modals import error_dialog from utils import human_readable from read_book.db import create_db +from read_book.view import View RENDER_VERSION = 1 # Also change this in render_book.py @@ -43,6 +44,7 @@ class ReadUI: container.appendChild(E.div( id=self.display_id, style='display:none', )) + self.view = View(container.lastChild) def show_stack(self, name): ans = None @@ -145,6 +147,7 @@ class ReadUI: def download_book(self, book): files = book.manifest.files total = 0 + cover_total_updated = False for name in files: total += files[name].size files_left = set(book.manifest.files) @@ -161,6 +164,7 @@ class ReadUI: query = {'library_id': library_id} progress_track = {} pbar.setAttribute('max', total + '') + raster_cover_name = book.manifest.raster_cover_name def update_progress(): x = 0 @@ -190,22 +194,34 @@ class ReadUI: files_left.discard(this) return if end_type is 'load': - self.db.store_file(book, this, xhr, on_stored.bind(this)) + self.db.store_file(book, this, xhr, on_stored.bind(this), this is raster_cover_name) else: failed_files.append([this, xhr.error_html]) files_left.discard(this) - def on_progress(loaded): + def on_progress(loaded, ftotal): + nonlocal total, cover_total_updated + if this is raster_cover_name and not cover_total_updated: + cover_total_updated = True + total = total - files[raster_cover_name].size + ftotal + pbar.setAttribute('max', total + '') progress_track[this] = loaded update_progress() - for fname in files_left: - xhr = ajax(base_path + encodeURIComponent(fname), on_complete.bind(fname), on_progress=on_progress.bind(fname), query=query, progress_totals_needed=False) + def start_download(fname, path): + xhr = ajax(path, on_complete.bind(fname), on_progress=on_progress.bind(fname), query=query, progress_totals_needed=False) xhr.responseType = 'text' if not book.manifest.files[name].is_virtualized: xhr.responseType = 'blob' if self.db.supports_blobs else 'arraybuffer' xhr.send() self.downloads_in_progress.append(xhr) + if raster_cover_name: + start_download(raster_cover_name, 'get/cover/' + book_id + '/' + encodeURIComponent(library_id)) + + for fname in files_left: + if fname is not raster_cover_name: + start_download(fname, base_path + encodeURIComponent(fname)) + def display_book(self, book): self.show_stack(self.display_id) diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj new file mode 100644 index 0000000000..96d08bc79b --- /dev/null +++ b/src/pyj/read_book/view.pyj @@ -0,0 +1,30 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +from elementmaker import E +from gettext import gettext as _ + +LOADING_DOC = ''' +

{}

+''' +class View: + + def __init__(self, container): + self.iframe_id = 'read-book-iframe' + container.appendChild( + E.iframe( + id=self.iframe_id, + seamless=True, + sandbox='allow-popups allow-scripts', + ) + ) + self.show_loading() + + @property + def iframe(self): + return document.getElementById(self.iframe_id) + + def show_loading(self): + iframe = self.iframe + iframe.setAttribute('srcdoc', str.format(LOADING_DOC, _( + 'Loading, please wait...')))