diff --git a/src/calibre/srv/render_book.py b/src/calibre/srv/render_book.py index d9abff4427..78849f10fd 100644 --- a/src/calibre/srv/render_book.py +++ b/src/calibre/srv/render_book.py @@ -37,14 +37,12 @@ def decode_component(x): def encode_url(name, frag=''): name = encode_component(name) if frag: - name += '#' + encode_component(frag) + name += '#' + frag return name def decode_url(x): - parts = list(map(decode_component, x.split('#', 1))) - if len(parts) == 1: - parts.append('') - return parts + parts = x.split('#', 1) + return decode_component(parts[0]), parts[1] or '' class Container(ContainerBase): diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 00847e0c88..0f8ee994b6 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -2,7 +2,9 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal from aes import GCM +from gettext import install from read_book.globals import set_boss +from read_book.resources import finalize_resources class Boss: @@ -17,7 +19,8 @@ class Boss: ) set_boss(self) self.handlers = { - 'keys':self.create_gcm.bind(self), + 'initialize':self.initialize.bind(self), + 'display': self.display.bind(self), } def handle_message(self, event): @@ -33,12 +36,24 @@ class Boss: return func = self.handlers[data.action] if func: - func(data) + try: + func(data) + except Exception as e: + console.log('Error in iframe message handler:') + console.log(e) + self.send_message({'action':'error', 'details':e.stack, 'msg':e.toString()}) else: print('Unknown action in message to iframe from parent: ' + data.action) - def create_gcm(self, data): + def initialize(self, data): self.gcm_from_parent, self.gcm_to_parent = GCM(data.secret.subarray(0, 32)), GCM(data.secret.subarray(32)) + install(data.translations) + + def display(self, data): + self.encrypted_communications = True + self.book = data.book + root_data = finalize_resources(self.book, data.name, data.resource_data) + root_data def send_message(self, data): if self.encrypted_communications: diff --git a/src/pyj/read_book/resources.pyj b/src/pyj/read_book/resources.pyj index 9aaa732101..0175ebb07f 100644 --- a/src/pyj/read_book/resources.pyj +++ b/src/pyj/read_book/resources.pyj @@ -1,125 +1,144 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal +from encodings import base64decode, utf8_decode + +JSON_XHTML_MIMETYPE = 'application/calibre+xhtml+json' def decode_component(x): - x = str.replace(x,',p', '|') - return str.replace(x, ',c', ',') + return utf8_decode(base64decode(x)) def decode_url(x): - parts = x.split(',,') - return decode_component(parts[0]), decode_component(parts[1] or '') + parts = x.split('#', 1) + return decode_component(parts[0]), parts[1] or '' -class Resource: +def create_link_pat(book): + return RegExp(book.manifest.link_uid + r'\|([^|]+)\|', 'g') - def __init__(self, name, mimetype, data, placeholder, parent): - self.name = name - self.placeholder = placeholder - if type(data) is 'string': - self.text = data - self.mimetype = mimetype - else: - if data: - self.url = window.URL.createObjectURL(data) - self.dependencies = [] - self.append = self.dependencies.append.bind(self.dependencies) - self.remove = self.dependencies.remove.bind(self.dependencies) - self.parent = parent - if parent: - parent.append(self) +def load_resources(db, book, root_name, previous_resources, proceed): + ans = {} + pending_resources = v'[root_name]' + link_pat = create_link_pat(book) - def transfer(self, parent): - self.parent.remove(self) - self.parent = parent - parent.append(self) - - def free(self): - if self.url: - window.URL.revokeObjectURL(self.url) - self.url = None - for child in self.dependencies: - child.free() - - def finalize(self): - if not self.text: + def do_one(): + if not pending_resources.length: + for k in previous_resources: + v'delete previous_resources[k]' + proceed(ans) return - for child in self.dependencies: - child.finalize() - if child.placeholder and child.url: - self.text = str.replace(self.text, child.placeholder, child.url) - self.url = window.createObjectURL(Blob([self.text], {'type':self.mimetype})) - self.text = None + name = pending_resources.shift() + if name in ans: + return setTimeout(do_one, 0) + if name in previous_resources: + ans[name] = data = previous_resources[name] + if type(data) is 'string': + find_virtualized_resources(data) + return setTimeout(do_one, 0) + db.get_file(book, name, got_one) - def find_match(self, name): - if self.name is name: - return self - for child in self.dependencies: - x = child.find_match(name) - if x: - return x - -class ResourceManager: - - def __init__(self): - self.root_resource = Resource() - self.pending_resources = [] - - def new_root(self, db, book, root_name, proceed): - self.db = db - self.book = book - self.root_name = root_name - self.proceed = proceed - self.old_root_resource = self.root_resource - self.root_resource = Resource() - self.pending_resources = [{'name':root_name, 'parent':self.root_resource, 'placeholder':None}] - self.link_pat = RegExp(book.manifest.link_uid + r'\|([^|]+)\|', 'g') - self.do_one() - - def do_one(self): - if not self.pending_resources.length: - self.root_resource.finalize() - self.old_root_resource.free() - self.old_root_resource = None - self.proceed(self.root_resource.dependencies[0].url) - - r = self.pending_resources.pypop(0) - if self.root_resource.find_match(r.name): - return self.do_one() - oldr = self.old_root_resource.find_match(r.name) - if oldr: - oldr.transfer(r.parent) - return self.do_one() - - self.db.get_file(self.book, r.name, self.got_one.bind(self, r)) - - def got_one(self, pending_resource, data, name, mimetype): - if name is self.root_name: - data = self.process_spine_item(data) - mimetype = 'application/xhtml+xml' - r = Resource(name, mimetype, data, pending_resource.placeholder, pending_resource.parent) + def got_one(data, name, mimetype): + if name is book.manifest.title_page_name: + w = book.manifest.cover_width or 600 + h = book.manifest.cover_height or 800 + ar = 'xMidYMid meet' # or 'none' + data = str.replace(data, '__ar__', ar) + data = str.replace(data, '__viewbox__', '0 0 ' + w + ' ' + h) + data = str.replace(data, '__width__', w + '') + data = str.replace(data, '__height__', h + '') + ans[name] = v'[data, mimetype]' if type(data) is 'string': - self.find_virtualized_resources(data, r) - self.do_one() + find_virtualized_resources(data) + return setTimeout(do_one, 0) - def find_virtualized_resources(self, text, parent): + def find_virtualized_resources(text): seen = set() + already_pending = {x.name for x in pending_resources} while True: - m = self.link_pat.exec(text) + m = link_pat.exec(text) if not m: break name = decode_url(m[1])[0] - if name in seen: + if name in seen or name in already_pending: continue seen.add(name) - self.pending_resources.push({'name':name, 'parent':parent, 'placeholder':m[0]}) + pending_resources.push(name) - def process_spine_item(self, text): - if self.root_name is self.book.manifest.title_page_name: - w = self.book.manifest.cover_width or 600 - h = self.book.manifest.cover_height or 800 - ar = 'xMidYMid meet' # or 'none' - text = str.replace(text, '__ar__', ar) - text = str.replace(text, '__viewbox__', '0 0 ' + w + ' ' + h) - text = str.replace(text, '__width__', w + '') - text = str.replace(text, '__height__', h + '') - return text + do_one() + +def finalize_resources(book, root_name, resource_data): + blob_url_map = {} + root_data = None + link_pat = create_link_pat(book) + + # Resolve the non virtualized resources immediately + for name in resource_data: + data, mimetype = resource_data[name] + if type(data) is not 'string': + blob_url_map[name] = window.URL.createObjectURL(data) + for name in blob_url_map: + v'delete resource_data[name]' + + def add_virtualized_resource(name, text, mimetype): + nonlocal root_data + if name is root_name: + root_data = JSON.parse(text) + else: + blob_url_map[name] = window.URL.createObjectURL(Blob([text], {'type': mimetype})) + + def replace_deps(text): + replacements = v'[]' + unresolved_deps = set() + while True: + m = link_pat.exec(text) + if not m: + break + dname, frag = decode_url(m[1]) + if dname in blob_url_map: + rtext = blob_url_map[dname] + if frag: + rtext += '#' + frag + replacements.push(v'[m.index, m[0].length, rtext]') + elif unresolved_deps: + unresolved_deps.add(dname) + for index, sz, repl in reversed(replacements): + text = text[:index] + repl + text[index:] + return unresolved_deps, text + + unresolved_deps_map = {} + + def has_unresolvable_deps(name): + deps = unresolved_deps_map[name] + if not deps or not deps.length: + return False + deps = [x for x in deps if x not in blob_url_map] + return deps.length > 0 + + while True: + resolved = v'[]' + num = 0 + for name in resource_data: + if name not in blob_url_map: + num += 1 + text, mimetype = resource_data[name] + if not has_unresolvable_deps(name): + unresolved_deps, text = replace_deps(text) + unresolved_deps_map[name] = unresolved_deps + if not unresolved_deps.length: + add_virtualized_resource(name, text, mimetype) + resolved.push(name) + if not num: + break + if not resolved.length: + unresolved = [name for name in resource_data if name not in blob_url_map] + print(str.format('ERROR: Could not resolve all dependencies of {} because of a cyclic dependency. Remaining deps: {}', root_name, unresolved)) + # Add the items anyway, without resolving remaining deps + for name in resource_data: + if name not in blob_url_map: + text, mimetype = resource_data[name] + text = replace_deps(text)[1] + add_virtualized_resource(name, text, mimetype) + break + for name in resolved: + v'delete resource_data[name]' + + return root_data diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index dd1abf2404..f0e6f6431a 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -4,7 +4,7 @@ from elementmaker import E from gettext import gettext as _ from read_book.globals import messenger, iframe_id -from read_book.resources import ResourceManager +from read_book.resources import load_resources LOADING_DOC = ''' @@ -15,7 +15,9 @@ __SCRIPT__ end_script +
__BS__ +
'''.replace('end_script', '<' + '/script>') # cannot have a closing script tag as this is embedded inside a script tag in index.html @@ -24,8 +26,7 @@ class View: def __init__(self, container, ui): self.ui = ui - self.resource_manager = ResourceManager() - self.virtualized_resources = {} + self.loaded_resources = {} container.appendChild( E.iframe( id=iframe_id, @@ -41,6 +42,7 @@ class View: window.addEventListener('message', self.handle_message.bind(self), False) self.handlers = { 'ready': self.on_iframe_ready.bind(self), + 'error': self.on_iframe_error.bind(self), } @property @@ -51,8 +53,8 @@ class View: iframe_script = self.ui.interface_data.main_js.replace(/is_running_in_iframe\s*=\s*false/, 'is_running_in_iframe = true') self.ui.interface_data.main_js = None self.src_doc = self.iframe.srcdoc = LOADING_DOC.replace( - '__SCRIPT__', iframe_script).replace( - '__BS__', _('Bootstrapping book reader...')) + '__BS__', _('Bootstrapping book reader...')).replace( + '__SCRIPT__', iframe_script) def init_iframe(self, iframe_script): self.encrypted_communications = False @@ -82,10 +84,15 @@ class View: def on_iframe_ready(self, data): messenger.reset() - self.send_message({'action':'keys', 'secret':messenger.secret}) + self.send_message({'action':'initialize', 'secret':messenger.secret, 'translations':self.ui.interface_data.translations}) self.iframe_ready = True if self.pending_spine_load: - self.show_spine_item_stage2() + data = self.pending_spine_load + self.pending_spine_load = None + self.show_spine_item_stage2(data) + + def on_iframe_error(self, data): + self.ui.show_error((data.title or _('There was an error processing the book')), data.msg, data.details) def show_loading(self, title): return # TODO: Implement this @@ -96,13 +103,16 @@ class View: self.ui.db.update_last_read_time(book) # TODO: Check for last open position of book name = book.manifest.spine[0] - self.resource_manager.new_root(self.ui.db, book, name, self.show_spine_item.bind(self)) + load_resources(self.ui.db, book, name, self.loaded_resources, self.show_spine_item.bind(self, name)) - def show_spine_item(self, resource_data): + def show_spine_item(self, name, resource_data): + self.loaded_resources = resource_data # Re-init the iframe to ensure any changes made to the environment by the last spine item are lost self.init_iframe() # Now wait for frame to message that it is ready - self.pending_spine_load = resource_data + self.pending_spine_load = [name, resource_data] - def show_spine_item_stage2(self): - pass + def show_spine_item_stage2(self, x): + name, resource_data = x + self.send_message({'action':'display', 'resource_data':resource_data, 'book':self.book, 'name':name}) + self.encrypted_communications = True