From 97ea2edc8f3af4cbbb3031317dcabb4284ed7707 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 24 Dec 2016 17:39:43 +0530 Subject: [PATCH] E-book viewer: Fix regression in previous release that broke text layout for some books. Fixes #1652408 [Last update does not always center the pages in the reader.](https://bugs.launchpad.net/calibre/+bug/1652408) --- resources/compiled_coffeescript.zip | Bin 101586 -> 101577 bytes src/calibre/ebooks/oeb/display/extract.coffee | 8 +- src/calibre/ebooks/oeb/display/mathjax.coffee | 4 +- src/calibre/ebooks/oeb/display/webview.py | 22 ++- src/calibre/gui2/viewer/documentview.py | 50 +++--- src/calibre/gui2/viewer/fake_net.py | 152 ++++++++++++++++++ src/calibre/gui2/viewer/footnote.py | 10 +- src/calibre/gui2/viewer/main.py | 8 +- 8 files changed, 213 insertions(+), 41 deletions(-) create mode 100644 src/calibre/gui2/viewer/fake_net.py diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 05815284123d37105442d21f67aa3fed36baf553..1934079e84f89f91a700f254c6f799c965df69db 100644 GIT binary patch delta 445 zcmcaKlkMb8wheBHf}d(ib z!qm%2%}X!I&{R;gwS~%RDpXIuU(YDDIX+Kb1fi#{$%YGNfTs2I24hB%%@>+fGx*d#u zjO(V?bTI15qZk7;N|pf`yqkWagVBrW$==DF`(&pZb~0))Zk-;}$>_oS&5?0>qc@}Q zbbTjA&gqANGR)E5U?JJ*yj_d}jOo+mx)`OVzt?8un0}y}(R%uZZbpvDxrtmlN>&O= zNfzcQsYZrLmT5_b=Ejzm76uln#^xr8iRR|U=1E4z#%UHwDV7$-(-(F#icgp8X7rr? zr;|}|`jQ?--RW<;855`HgTyX$G5SrG+s84zw~Ntvx?c|?pO$%&MM|Qng-NP~xuu0! gqOqB!fvJIIin+0gfrVj;p=GL(acWwUd74r!0AzEIJpcdz delta 507 zcmX>(lkL(>wheBHf>}!49-1CKW$QH=7(iHd`UP!9vFY#I8M!u>C-U-4?#Z1fsGgRY zlWL`}udbJqnwMUZp*ekG38OF%Tv$_~din-0MybugdGaEYtD0;WH76@Js&9VQB%~>r zysgE)N@6M_s3APli#v-}z<7Ad$13fc-K8JWd;Nr}a&K(@O6X15I`*LhJ~H~n}! zV+@m8_wk;V&s@!*vY8H^52n>VR~RUqxAG$KrVBNH{ trim = (str) -> return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '') -is_footnote_link = (node, url, linked_to_anchors) -> - if not url or url.substr(0, 'file://'.length).toLowerCase() != 'file://' +is_footnote_link = (node, url, linked_to_anchors, prefix) -> + if not url or url.substr(0, prefix.length) != prefix return false # Ignore non-local links epub_type = get_epub_type(node, ['noteref']) if epub_type and epub_type.toLowerCase() == 'noteref' @@ -163,8 +163,8 @@ class CalibreExtract cnode = inline_styles(node) return cnode.outerHTML - is_footnote_link: (a) -> - return is_footnote_link(a, a.href, py_bridge.value) + is_footnote_link: (a, prefix) -> + return is_footnote_link(a, a.href, py_bridge.value, prefix) show_footnote: (target, known_targets) -> if not target diff --git a/src/calibre/ebooks/oeb/display/mathjax.coffee b/src/calibre/ebooks/oeb/display/mathjax.coffee index 8a635d2f65..78cb80aee4 100644 --- a/src/calibre/ebooks/oeb/display/mathjax.coffee +++ b/src/calibre/ebooks/oeb/display/mathjax.coffee @@ -32,7 +32,7 @@ class MathJax scale = if is_windows then 160 else 100 script.type = 'text/javascript' - script.src = 'file://' + this.base + '/MathJax.js' + script.src = this.base + 'MathJax.js' script.text = user_config + (''' MathJax.Hub.signal.Interest(function (message) {if (String(message).match(/error/i)) {console.log(message)}}); MathJax.Hub.Config({ @@ -111,5 +111,3 @@ class MathJax if window? window.mathjax = new MathJax() - - diff --git a/src/calibre/ebooks/oeb/display/webview.py b/src/calibre/ebooks/oeb/display/webview.py index 44dcc97d95..9dfc43c9c4 100644 --- a/src/calibre/ebooks/oeb/display/webview.py +++ b/src/calibre/ebooks/oeb/display/webview.py @@ -33,9 +33,20 @@ def self_closing_sub(match): return '<%s%s>'%(match.group(1), match.group(2), match.group(1)) +def cleanup_html(html): + html = EntityDeclarationProcessor(html).processed_html + self_closing_pat = re.compile(r'<\s*([:A-Za-z0-9-]+)([^>]*)/\s*>') + html = self_closing_pat.sub(self_closing_sub, html) + return html + + +def load_as_html(html): + return re.search(r'<[a-zA-Z0-9-]+:svg', html) is None and ']*)/\s*>') - html = self_closing_pat.sub(self_closing_sub, html) - - loading_url = QUrl.fromLocalFile(path) + html = cleanup_html(html) + loading_url = loading_url or QUrl.fromLocalFile(path) pre_load_callback(loading_url) - if force_as_html or re.search(r'<[a-zA-Z0-9-]+:svg', html) is None and ' + +from __future__ import (unicode_literals, division, absolute_import, + print_function) +import os + +from PyQt5.Qt import QNetworkReply, QNetworkAccessManager, QUrl, QNetworkRequest, QTimer, pyqtSignal + +from calibre import guess_type as _guess_type, prints +from calibre.constants import FAKE_HOST, FAKE_PROTOCOL, DEBUG +from calibre.ebooks.oeb.base import OEB_DOCS +from calibre.ebooks.oeb.display.webview import cleanup_html, load_as_html +from calibre.utils.short_uuid import uuid4 + + +def guess_type(x): + return _guess_type(x)[0] or 'application/octet-stream' + + +class NetworkReply(QNetworkReply): + + def __init__(self, parent, request, mime_type, data): + QNetworkReply.__init__(self, parent) + self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered) + self.setRequest(request) + self.setUrl(request.url()) + self._aborted = False + self.__data = data + self.setHeader(QNetworkRequest.ContentTypeHeader, mime_type) + self.setHeader(QNetworkRequest.ContentLengthHeader, len(self.__data)) + QTimer.singleShot(0, self.finalize_reply) + + def bytesAvailable(self): + return len(self.__data) + + def isSequential(self): + return True + + def abort(self): + pass + + def readData(self, maxlen): + if maxlen >= len(self.__data): + ans, self.__data = self.__data, b'' + return ans + ans, self.__data = self.__data[:maxlen], self.__data[maxlen:] + return ans + read = readData + + def finalize_reply(self): + self.setFinished(True) + self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200) + self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok") + self.metaDataChanged.emit() + self.downloadProgress.emit(len(self.__data), len(self.__data)) + self.readyRead.emit() + self.finished.emit() + + +def normpath(p): + return os.path.normcase(os.path.abspath(p)) + + +class NetworkAccessManager(QNetworkAccessManager): + + load_error = pyqtSignal(object, object) + + def __init__(self, parent=None): + QNetworkAccessManager.__init__(self, parent) + self.mathjax_prefix = str(uuid4()) + self.mathjax_base = '%s://%s/%s/' % (FAKE_PROTOCOL, FAKE_HOST, self.mathjax_prefix) + self.root = self.orig_root = os.path.dirname(P('viewer/blank.html', allow_user_override=False)) + self.mime_map, self.single_pages, self.codec_map = {}, set(), {} + + def set_book_data(self, root, spine): + self.orig_root = root + self.root = os.path.normcase(os.path.abspath(root)) + self.mime_map, self.single_pages, self.codec_map = {}, set(), {} + for p in spine: + mt = getattr(p, 'mime_type', None) + key = normpath(p) + if mt is not None: + self.mime_map[key] = mt + self.codec_map[key] = getattr(p, 'encoding', 'utf-8') + if getattr(p, 'is_single_page', False): + self.single_pages.add(key) + + def is_single_page(self, path): + if not path: + return False + key = normpath(path) + return key in self.single_pages + + def as_abspath(self, qurl): + name = qurl.path()[1:] + return os.path.join(self.orig_root, *name.split('/')) + + def as_url(self, abspath): + name = os.path.relpath(abspath, self.root).replace('\\', '/') + ans = QUrl() + ans.setScheme(FAKE_PROTOCOL), ans.setAuthority(FAKE_HOST), ans.setPath('/' + name) + return ans + + def guess_type(self, name): + mime_type = guess_type(name) + mime_type = { + # Prevent warning in console about mimetype of fonts + 'application/vnd.ms-opentype':'application/x-font-ttf', + 'application/x-font-truetype':'application/x-font-ttf', + 'application/x-font-opentype':'application/x-font-ttf', + 'application/x-font-otf':'application/x-font-ttf', + 'application/font-sfnt': 'application/x-font-ttf', + }.get(mime_type, mime_type) + return mime_type + + def preprocess_data(self, data, path): + mt = self.mime_map.get(path, self.guess_type(path)) + if mt.lower() in OEB_DOCS: + enc = self.codec_map.get(path, 'utf-8') + html = data.decode(enc, 'replace') + html = cleanup_html(html) + data = html.encode('utf-8') + if load_as_html(html): + mt = 'text/html; charset=utf-8' + else: + mt = 'application/xhtml+xml; charset=utf-8' + return data, mt + + def createRequest(self, operation, request, data): + qurl = request.url() + if operation == QNetworkAccessManager.GetOperation and qurl.host() == FAKE_HOST: + name = qurl.path()[1:] + if name.startswith(self.mathjax_prefix): + base = normpath(P('viewer/mathjax')) + path = normpath(os.path.join(base, name.partition('/')[2])) + else: + base = self.root + path = normpath(os.path.join(self.root, name)) + if path.startswith(base) and os.path.exists(path): + try: + with lopen(path, 'rb') as f: + data = f.read() + data, mime_type = self.preprocess_data(data, path) + return NetworkReply(self, request, mime_type, data) + except Exception: + import traceback + self.load_error.emit(name, traceback.format_exc()) + if DEBUG: + prints('URL not found in book: %r' % qurl.toString()) + return QNetworkAccessManager.createRequest(self, operation, request) diff --git a/src/calibre/gui2/viewer/footnote.py b/src/calibre/gui2/viewer/footnote.py index 26d8f788cc..e829eb02ad 100644 --- a/src/calibre/gui2/viewer/footnote.py +++ b/src/calibre/gui2/viewer/footnote.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -import json, os +import json from collections import defaultdict from PyQt5.Qt import ( @@ -16,7 +16,7 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage from PyQt5.QtWebKit import QWebSettings from calibre import prints -from calibre.constants import DEBUG +from calibre.constants import DEBUG, FAKE_PROTOCOL, FAKE_HOST from calibre.ebooks.oeb.display.webview import load_html @@ -124,10 +124,10 @@ class Footnotes(object): pass def get_footnote_data(self, a, qurl): - current_path = os.path.abspath(unicode(self.view.document.mainFrame().baseUrl().toLocalFile())) + current_path = self.view.path() if not current_path: return # Not viewing a local file - dest_path = self.spine_path(os.path.abspath(unicode(qurl.toLocalFile()))) + dest_path = self.spine_path(self.view.path(qurl)) if dest_path is not None: if dest_path == current_path: # We deliberately ignore linked to anchors if the destination is @@ -138,7 +138,7 @@ class Footnotes(object): else: linked_to_anchors = {anchor:0 for path, anchor in dest_path.verified_links if path == current_path} self.view.document.bridge_value = linked_to_anchors - if a.evaluateJavaScript('calibre_extract.is_footnote_link(this)'): + if a.evaluateJavaScript('calibre_extract.is_footnote_link(this, "%s://%s")' % (FAKE_PROTOCOL, FAKE_HOST)): if dest_path not in self.known_footnote_targets: self.known_footnote_targets[dest_path] = s = set() for item in self.view.manager.iterator.spine: diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index bedd840b4d..0deef2ec1c 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -7,7 +7,7 @@ from threading import Thread from PyQt5.Qt import ( QApplication, Qt, QIcon, QTimer, QByteArray, QSize, QTime, QObject, - QPropertyAnimation, QUrl, QInputDialog, QAction, QModelIndex, pyqtSignal) + QPropertyAnimation, QInputDialog, QAction, QModelIndex, pyqtSignal) from calibre.gui2.viewer.ui import Main as MainWindow from calibre.gui2.viewer.toc import TOC @@ -549,7 +549,7 @@ class EbookViewer(MainWindow): return error_dialog(self, _('No such location'), _('The location pointed to by this item' ' does not exist.'), det_msg=item.abspath, show=True) - url = QUrl.fromLocalFile(item.abspath) + url = self.view.as_url(item.abspath) if item.fragment: url.setFragment(item.fragment) self.link_clicked(url) @@ -664,7 +664,7 @@ class EbookViewer(MainWindow): self.history.add(prev_pos) def link_clicked(self, url): - path = os.path.abspath(unicode(url.toLocalFile())) + path = self.view.path(url) frag = None if path in self.iterator.spine: self.update_page_number() # Ensure page number is accurate as it is used for history @@ -986,6 +986,7 @@ class EbookViewer(MainWindow): vh.insert(0, pathtoebook) vprefs.set('viewer_open_history', vh[:50]) self.build_recent_menu() + self.view.set_book_data(self.iterator) self.footnotes_dock.close() self.action_table_of_contents.setDisabled(not self.iterator.toc) @@ -1266,5 +1267,6 @@ def main(args=sys.argv): return app.exec_() return 0 + if __name__ == '__main__': sys.exit(main())