Use the cover in the db rather than the one in the file when rendering books

This commit is contained in:
Kovid Goyal 2016-03-23 20:25:12 +05:30
parent 1c8058fff6
commit c41e4b00fc
5 changed files with 150 additions and 23 deletions

View File

@ -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.conversion.config import load_defaults
from calibre.ebooks.oeb.transforms.cover import CoverManager from calibre.ebooks.oeb.transforms.cover import CoverManager
try:
ext = cover_path.rpartition('.')[-1].lower() ext = cover_path.rpartition('.')[-1].lower()
except Exception:
ext = 'jpeg'
cname, tname = 'cover.' + ext, 'titlepage.xhtml' cname, tname = 'cover.' + ext, 'titlepage.xhtml'
recommended_folders = get_recommended_folders(container, (cname, tname)) recommended_folders = get_recommended_folders(container, (cname, tname))
@ -273,7 +276,11 @@ def create_epub_cover(container, cover_path, existing_image, options=None):
raster_cover_item = container.generate_item(cname, id_prefix='cover') raster_cover_item = container.generate_item(cname, id_prefix='cover')
raster_cover = container.href_to_name(raster_cover_item.get('href'), container.opf_name) 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: 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) shutil.copyfileobj(src, dest)
if options is None: if options is None:
opts = load_defaults('epub_output') opts = load_defaults('epub_output')
@ -285,6 +292,9 @@ def create_epub_cover(container, cover_path, existing_image, options=None):
if no_svg: if no_svg:
style = 'style="height: 100%%"' style = 'style="height: 100%%"'
templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style) templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style)
else:
if callable(cover_path):
templ = CoverManager.SVG_TEMPLATE
else: else:
width, height = 600, 800 width, height = 600, 800
try: try:
@ -412,5 +422,6 @@ def set_epub_cover(container, cover_path, report, options=None):
if s is not None and s != d} if s is not None and s != d}
if link_sub: if link_sub:
replace_links(container, link_sub, frag_map=lambda x, y:None) replace_links(container, link_sub, frag_map=lambda x, y:None)
return raster_cover, titlepage

View File

@ -12,15 +12,18 @@ from urlparse import urlparse
from cssutils import replaceUrls from cssutils import replaceUrls
from lxml.etree import Comment, tostring 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.iterator.book import extract_book
from calibre.ebooks.oeb.polish.container import Container as ContainerBase 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.toc import get_toc
from calibre.ebooks.oeb.polish.utils import guess_type from calibre.ebooks.oeb.polish.utils import guess_type
from calibre.utils.short_uuid import uuid4 from calibre.utils.short_uuid import uuid4
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
RENDER_VERSION = 1 # Also change this in read_book.ui.pyj 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): def encode_component(x):
return x.replace(',', ',c').replace('|', ',p') 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 == self.opf_name or mt == guess_type('a.ncx') or name.startswith('META-INF/') or
name == 'mimetype' name == 'mimetype'
} }
raster_cover_name, titlepage_name = self.create_cover_page(input_fmt.lower())
self.book_render_data = data = { self.book_render_data = data = {
'version': RENDER_VERSION, 'version': RENDER_VERSION,
@ -61,10 +65,13 @@ class Container(ContainerBase):
'link_uid': uuid4(), 'link_uid': uuid4(),
'book_hash': book_hash, 'book_hash': book_hash,
'is_comic': input_fmt.lower() in {'cbc', 'cbz', 'cbr', 'cb7'}, '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 # Mark the spine as dirty since we have to ensure it is normalized
for name in data['spine']: for name in data['spine']:
self.parsed(name), self.dirty(name) self.parsed(name), self.dirty(name)
self.inject_script(data['spine'])
self.virtualized_names = set() self.virtualized_names = set()
self.virtualize_resources() self.virtualize_resources()
def manifest_data(name): 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: 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')) 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): def virtualize_resources(self):
changed = set() changed = set()

View File

@ -82,6 +82,8 @@ class DB:
'last_read': Date(), 'last_read': Date(),
'metadata': metadata, 'metadata': metadata,
'manifest': None, 'manifest': None,
'cover_width': None,
'cover_height': None
}) })
) )
@ -94,12 +96,42 @@ class DB:
v'delete manifest["metadata"]' v'delete manifest["metadata"]'
self.do_op(['books'], book, _('Failed to write to the books database'), proceed, op='put') 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' store_as_text = xhr.responseType is 'text'
fname = file_store_name(book, name) fname = file_store_name(book, name)
needs_encoding = not store_as_text and not self.supports_blobs 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} 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: if needs_encoding:
data = base64encode(Uint8Array(data)) data = base64encode(Uint8Array(data))
req = self.idb.transaction(['files'], 'readwrite').objectStore('files').put(data, fname) req = self.idb.transaction(['files'], 'readwrite').objectStore('files').put(data, fname)

View File

@ -8,6 +8,7 @@ from book_list.globals import get_boss
from modals import error_dialog from modals import error_dialog
from utils import human_readable from utils import human_readable
from read_book.db import create_db from read_book.db import create_db
from read_book.view import View
RENDER_VERSION = 1 # Also change this in render_book.py RENDER_VERSION = 1 # Also change this in render_book.py
@ -43,6 +44,7 @@ class ReadUI:
container.appendChild(E.div( container.appendChild(E.div(
id=self.display_id, style='display:none', id=self.display_id, style='display:none',
)) ))
self.view = View(container.lastChild)
def show_stack(self, name): def show_stack(self, name):
ans = None ans = None
@ -145,6 +147,7 @@ class ReadUI:
def download_book(self, book): def download_book(self, book):
files = book.manifest.files files = book.manifest.files
total = 0 total = 0
cover_total_updated = False
for name in files: for name in files:
total += files[name].size total += files[name].size
files_left = set(book.manifest.files) files_left = set(book.manifest.files)
@ -161,6 +164,7 @@ class ReadUI:
query = {'library_id': library_id} query = {'library_id': library_id}
progress_track = {} progress_track = {}
pbar.setAttribute('max', total + '') pbar.setAttribute('max', total + '')
raster_cover_name = book.manifest.raster_cover_name
def update_progress(): def update_progress():
x = 0 x = 0
@ -190,22 +194,34 @@ class ReadUI:
files_left.discard(this) files_left.discard(this)
return return
if end_type is 'load': 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: else:
failed_files.append([this, xhr.error_html]) failed_files.append([this, xhr.error_html])
files_left.discard(this) 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 progress_track[this] = loaded
update_progress() update_progress()
for fname in files_left: def start_download(fname, path):
xhr = ajax(base_path + encodeURIComponent(fname), on_complete.bind(fname), on_progress=on_progress.bind(fname), query=query, progress_totals_needed=False) xhr = ajax(path, on_complete.bind(fname), on_progress=on_progress.bind(fname), query=query, progress_totals_needed=False)
xhr.responseType = 'text' xhr.responseType = 'text'
if not book.manifest.files[name].is_virtualized: if not book.manifest.files[name].is_virtualized:
xhr.responseType = 'blob' if self.db.supports_blobs else 'arraybuffer' xhr.responseType = 'blob' if self.db.supports_blobs else 'arraybuffer'
xhr.send() xhr.send()
self.downloads_in_progress.append(xhr) 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): def display_book(self, book):
self.show_stack(self.display_id) self.show_stack(self.display_id)

View File

@ -0,0 +1,30 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from elementmaker import E
from gettext import gettext as _
LOADING_DOC = '''
<p>{}</p>
'''
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...')))