Fix a regression in 6.0 that broke rendering of first page of EPUB as cover when the EPUB has no actual cover

This commit is contained in:
Kovid Goyal 2022-07-18 15:28:55 +05:30
parent a41c3b775c
commit ba8f4fb7ae
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
12 changed files with 138 additions and 55 deletions

View File

@ -89,7 +89,7 @@ def extract_calibre_cover(raw, base, log):
return return_raster_image(img) return return_raster_image(img)
def render_html_svg_workaround(path_to_html, log, width=590, height=750): def render_html_svg_workaround(path_to_html, log, width=590, height=750, root=''):
from calibre.ebooks.oeb.base import SVG_NS from calibre.ebooks.oeb.base import SVG_NS
with open(path_to_html, 'rb') as f: with open(path_to_html, 'rb') as f:
raw = f.read() raw = f.read()
@ -108,11 +108,11 @@ def render_html_svg_workaround(path_to_html, log, width=590, height=750):
pass pass
if data is None: if data is None:
data = render_html_data(path_to_html, width, height) data = render_html_data(path_to_html, width, height, root=root)
return data return data
def render_html_data(path_to_html, width, height): def render_html_data(path_to_html, width, height, root=''):
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.utils.ipc.simple_worker import fork_job, WorkerError from calibre.utils.ipc.simple_worker import fork_job, WorkerError
result = {} result = {}
@ -127,7 +127,7 @@ def render_html_data(path_to_html, width, height):
with TemporaryDirectory('-render-html') as tdir: with TemporaryDirectory('-render-html') as tdir:
try: try:
result = fork_job('calibre.ebooks.render_html', 'main', args=(path_to_html, tdir, 'jpeg')) result = fork_job('calibre.ebooks.render_html', 'main', args=(path_to_html, tdir, 'jpeg', root))
except WorkerError as e: except WorkerError as e:
report_error(e.orig_tb) report_error(e.orig_tb)
else: else:

View File

@ -218,7 +218,7 @@ class EPUBInput(InputFormatPlugin):
elem[0].getparent(), OPF('item'), href=guide_elem.get('href'), id='calibre_raster_cover') elem[0].getparent(), OPF('item'), href=guide_elem.get('href'), id='calibre_raster_cover')
t.set('media-type', 'image/jpeg') t.set('media-type', 'image/jpeg')
if os.path.exists(guide_cover): if os.path.exists(guide_cover):
renderer = render_html_svg_workaround(guide_cover, log) renderer = render_html_svg_workaround(guide_cover, log, root=os.getcwd())
if renderer is not None: if renderer is not None:
with lopen('calibre_raster_cover.jpg', 'wb') as f: with lopen('calibre_raster_cover.jpg', 'wb') as f:
f.write(renderer) f.write(renderer)

View File

@ -146,16 +146,11 @@ class PDFOutput(OutputFormatPlugin):
# Ensure Qt is setup to be used with WebEngine # Ensure Qt is setup to be used with WebEngine
# specialize_options is called early enough in the pipeline # specialize_options is called early enough in the pipeline
# that hopefully no Qt application has been constructed as yet # that hopefully no Qt application has been constructed as yet
from qt.webengine import QWebEngineUrlScheme
from qt.webengine import QWebEnginePage # noqa from qt.webengine import QWebEnginePage # noqa
from calibre.gui2 import must_use_qt from calibre.gui2 import must_use_qt
from calibre.constants import FAKE_PROTOCOL from calibre.utils.webengine import setup_fake_protocol, setup_default_profile
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii')) setup_fake_protocol()
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
must_use_qt() must_use_qt()
from calibre.utils.webengine import setup_default_profile
setup_default_profile() setup_default_profile()
self.input_fmt = input_fmt self.input_fmt = input_fmt

View File

@ -193,7 +193,7 @@ def render_cover(cpage, zf, reader=None):
cpage = os.path.join(tdir, cpage) cpage = os.path.join(tdir, cpage)
if not os.path.exists(cpage): if not os.path.exists(cpage):
return return
return render_html_svg_workaround(cpage, default_log) return render_html_svg_workaround(cpage, default_log, root=tdir)
def get_cover(raster_cover, first_spine_item, reader): def get_cover(raster_cover, first_spine_item, reader):

View File

@ -625,7 +625,7 @@ class OEBReader:
writer = OEBWriter() writer = OEBWriter()
writer(self.oeb, tdir) writer(self.oeb, tdir)
path = os.path.join(tdir, unquote(hcover.href)) path = os.path.join(tdir, unquote(hcover.href))
data = render_html_svg_workaround(path, self.logger) data = render_html_svg_workaround(path, self.logger, root=tdir)
if not data: if not data:
data = b'' data = b''
id, href = self.oeb.manifest.generate('cover', 'cover.jpg') id, href = self.oeb.manifest.generate('cover', 'cover.jpg')

View File

@ -4,26 +4,88 @@
import os import os
import sys import sys
from qt.core import ( from qt.core import (
QApplication, QMarginsF, QPageLayout, QPageSize, Qt, QTimer, QUrl QApplication, QByteArray, QMarginsF, QPageLayout, QPageSize, Qt, QTimer, QUrl
)
from qt.webengine import (
QWebEnginePage, QWebEngineProfile, QWebEngineScript,
QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestJob,
QWebEngineUrlSchemeHandler
) )
from qt.webengine import QWebEnginePage, QWebEngineScript
from calibre.constants import FAKE_HOST, FAKE_PROTOCOL
from calibre.ebooks.metadata.pdf import page_images from calibre.ebooks.metadata.pdf import page_images
from calibre.ebooks.oeb.polish.utils import guess_type
from calibre.gui2 import must_use_qt from calibre.gui2 import must_use_qt
from calibre.utils.webengine import secure_webengine from calibre.gui_launch import setup_qt_logging
from calibre.utils.filenames import atomic_rename from calibre.utils.filenames import atomic_rename
from calibre.utils.logging import default_log
from calibre.utils.monotonic import monotonic from calibre.utils.monotonic import monotonic
from calibre.utils.webengine import (
secure_webengine, send_reply, setup_fake_protocol, setup_profile
)
LOAD_TIMEOUT = 20 LOAD_TIMEOUT = 20
PRINT_TIMEOUT = 10 PRINT_TIMEOUT = 10
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
def interceptRequest(self, request_info):
method = bytes(request_info.requestMethod())
if method not in (b'GET', b'HEAD'):
default_log.warn(f'Blocking URL request with method: {method}')
request_info.block(True)
return
qurl = request_info.requestUrl()
if qurl.scheme() not in (FAKE_PROTOCOL,):
default_log.warn(f'Blocking URL request {qurl.toString()} as it is not for a resource related to the HTML file being rendered')
request_info.block(True)
return
class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
def __init__(self, root, parent=None):
self.root = root
super().__init__(parent)
self.allowed_hosts = (FAKE_HOST,)
def requestStarted(self, rq):
if bytes(rq.requestMethod()) != b'GET':
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.RequestDenied)
url = rq.requestUrl()
host = url.host()
if host not in self.allowed_hosts or url.scheme() != FAKE_PROTOCOL:
return self.fail_request(rq)
path = url.path()
rp = path[1:]
if not rp:
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.UrlNotFound)
resolved_path = os.path.abspath(os.path.join(self.root, rp.replace('/', os.sep)))
if not resolved_path.startswith(self.root):
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.UrlNotFound)
try:
with open(resolved_path, 'rb') as f:
data = f.read()
except OSError as err:
default_log(f'Failed to read file: {rp} with error: {err}')
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.RequestFailed)
send_reply(rq, guess_type(os.path.basename(resolved_path)), data)
def fail_request(self, rq, fail_code=None):
if fail_code is None:
fail_code = QWebEngineUrlRequestJob.Error.UrlNotFound
rq.fail(fail_code)
print(f"Blocking FAKE_PROTOCOL request: {rq.requestUrl().toString()} with code: {fail_code}", file=sys.stderr)
class Render(QWebEnginePage): class Render(QWebEnginePage):
def __init__(self): def __init__(self, profile):
QWebEnginePage.__init__(self) QWebEnginePage.__init__(self, profile, QApplication.instance())
secure_webengine(self) secure_webengine(self)
self.printing_started = False self.printing_started = False
self.loadFinished.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection) self.loadFinished.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection)
@ -32,6 +94,11 @@ class Render(QWebEnginePage):
t.setInterval(500) t.setInterval(500)
t.timeout.connect(self.hang_check) t.timeout.connect(self.hang_check)
def break_cycles(self):
self.hang_timer.timeout.disconnect()
self.pdfPrintingFinished.disconnect()
self.setParent(None)
def load_finished(self, ok): def load_finished(self, ok):
if ok: if ok:
self.runJavaScript(''' self.runJavaScript('''
@ -52,8 +119,10 @@ class Render(QWebEnginePage):
def javaScriptConsoleMessage(self, level, msg, linenumber, source_id): def javaScriptConsoleMessage(self, level, msg, linenumber, source_id):
pass pass
def start_load(self, path_to_html): def start_load(self, path_to_html, root):
self.load(QUrl.fromLocalFile(path_to_html)) url = QUrl(f'{FAKE_PROTOCOL}://{FAKE_HOST}')
url.setPath('/' + os.path.relpath(path_to_html, root).replace(os.sep, '/'))
self.setUrl(url)
self.start_time = monotonic() self.start_time = monotonic()
self.hang_timer.start() self.hang_timer.start()
@ -79,7 +148,9 @@ class Render(QWebEnginePage):
if type(getattr(QPageSize, sz, None)) is type(QPageSize.PageSizeId.A4): # noqa if type(getattr(QPageSize, sz, None)) is type(QPageSize.PageSizeId.A4): # noqa
page_size = QPageSize(getattr(QPageSize, sz)) page_size = QPageSize(getattr(QPageSize, sz))
else: else:
from calibre.ebooks.pdf.image_writer import parse_pdf_page_size from calibre.ebooks.pdf.image_writer import (
parse_pdf_page_size
)
ps = parse_pdf_page_size(sz, data.get('unit', 'inch')) ps = parse_pdf_page_size(sz, data.get('unit', 'inch'))
if ps is not None: if ps is not None:
page_size = ps page_size = ps
@ -95,17 +166,25 @@ class Render(QWebEnginePage):
self.hang_timer.stop() self.hang_timer.stop()
def main(path_to_html, tdir, image_format='jpeg'): def main(path_to_html, tdir, image_format='jpeg', root=''):
if image_format not in ('jpeg', 'png'): if image_format not in ('jpeg', 'png'):
raise ValueError('Image format must be either jpeg or png') raise ValueError('Image format must be either jpeg or png')
must_use_qt() must_use_qt()
from calibre.utils.webengine import setup_default_profile setup_qt_logging()
setup_default_profile() setup_fake_protocol()
profile = setup_profile(QWebEngineProfile(QApplication.instance()))
path_to_html = os.path.abspath(path_to_html) path_to_html = os.path.abspath(path_to_html)
url_handler = UrlSchemeHandler(root or os.path.dirname(path_to_html), parent=profile)
interceptor = RequestInterceptor(profile)
profile.installUrlSchemeHandler(QByteArray(FAKE_PROTOCOL.encode('ascii')), url_handler)
profile.setUrlRequestInterceptor(interceptor)
os.chdir(tdir) os.chdir(tdir)
renderer = Render() renderer = Render(profile)
renderer.start_load(path_to_html) renderer.start_load(path_to_html, url_handler.root)
ret = QApplication.instance().exec() ret = QApplication.instance().exec()
renderer.break_cycles()
del renderer
if ret == 0: if ret == 0:
page_images('rendered.pdf', image_format=image_format) page_images('rendered.pdf', image_format=image_format)
ext = {'jpeg': 'jpg'}.get(image_format, image_format) ext = {'jpeg': 'jpg'}.get(image_format, image_format)

View File

@ -1188,8 +1188,9 @@ def main(shm_name=None):
override = 'calibre-gui' if islinux else None override = 'calibre-gui' if islinux else None
app = Application([], override_program_name=override) app = Application([], override_program_name=override)
from calibre.utils.webengine import setup_default_profile from calibre.utils.webengine import setup_default_profile, setup_fake_protocol
setup_default_profile() setup_default_profile()
setup_fake_protocol()
d = TOCEditor(path, title=title, write_result_to=path + '.result') d = TOCEditor(path, title=title, write_result_to=path + '.result')
d.start() d.start()
ok = 0 ok = 0

View File

@ -8,7 +8,7 @@ import time
from qt.core import QIcon from qt.core import QIcon
from calibre.constants import EDITOR_APP_UID, FAKE_PROTOCOL, islinux from calibre.constants import EDITOR_APP_UID, islinux
from calibre.ebooks.oeb.polish.check.css import shutdown as shutdown_css_check_pool from calibre.ebooks.oeb.polish.check.css import shutdown as shutdown_css_check_pool
from calibre.gui2 import ( from calibre.gui2 import (
Application, decouple, set_gui_prefs, setup_gui_option_parser Application, decouple, set_gui_prefs, setup_gui_option_parser
@ -50,14 +50,11 @@ def gui_main(path=None, notify=None):
def _run(args, notify=None): def _run(args, notify=None):
from qt.webengine import QWebEngineUrlScheme from calibre.utils.webengine import setup_fake_protocol
# Ensure we can continue to function if GUI is closed # Ensure we can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir() reset_base_dir()
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii')) setup_fake_protocol()
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
# The following two lines are needed to prevent circular imports causing # The following two lines are needed to prevent circular imports causing
# errors during initialization of plugins that use the polish container # errors during initialization of plugins that use the polish container

View File

@ -8,7 +8,7 @@ import sys
from contextlib import closing from contextlib import closing
from qt.core import QIcon, QObject, Qt, QTimer, pyqtSignal from qt.core import QIcon, QObject, Qt, QTimer, pyqtSignal
from calibre.constants import FAKE_PROTOCOL, VIEWER_APP_UID, islinux from calibre.constants import VIEWER_APP_UID, islinux
from calibre.gui2 import Application, error_dialog, setup_gui_option_parser from calibre.gui2 import Application, error_dialog, setup_gui_option_parser
from calibre.gui2.listener import send_message_in_process from calibre.gui2.listener import send_message_in_process
from calibre.gui2.viewer.config import get_session_pref, vprefs from calibre.gui2.viewer.config import get_session_pref, vprefs
@ -167,14 +167,11 @@ def run_gui(app, opts, args, internal_book_data, listener=None):
def main(args=sys.argv): def main(args=sys.argv):
from qt.webengine import QWebEngineUrlScheme from calibre.utils.webengine import setup_fake_protocol
# Ensure viewer can continue to function if GUI is closed # Ensure viewer can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir() reset_base_dir()
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii')) setup_fake_protocol()
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
override = 'calibre-ebook-viewer' if islinux else None override = 'calibre-ebook-viewer' if islinux else None
processed_args = [] processed_args = []
internal_book_data = internal_book_data_path = None internal_book_data = internal_book_data_path = None

View File

@ -16,11 +16,11 @@ from calibre.utils.ipc.simple_worker import start_pipe_worker
def worker_main(source): def worker_main(source):
from qt.core import QLoggingCategory, QUrl from qt.core import QUrl
QLoggingCategory.setFilterRules('''\
qt.webenginecontext.info=false
''')
from calibre.gui2 import must_use_qt from calibre.gui2 import must_use_qt
from calibre.gui_launch import setup_qt_logging
setup_qt_logging()
from .simple_backend import SimpleScraper from .simple_backend import SimpleScraper
must_use_qt() must_use_qt()

View File

@ -59,7 +59,9 @@ def compiler():
from calibre import walk from calibre import walk
from calibre.gui2 import must_use_qt from calibre.gui2 import must_use_qt
from calibre.utils.webengine import secure_webengine, setup_default_profile, setup_profile from calibre.utils.webengine import (
secure_webengine, setup_default_profile, setup_profile
)
must_use_qt() must_use_qt()
setup_default_profile() setup_default_profile()
@ -335,7 +337,7 @@ def run_rapydscript_tests():
from qt.core import QApplication, QByteArray, QEventLoop, QUrl from qt.core import QApplication, QByteArray, QEventLoop, QUrl
from qt.webengine import ( from qt.webengine import (
QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineUrlRequestJob, QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineUrlRequestJob,
QWebEngineUrlScheme, QWebEngineUrlSchemeHandler QWebEngineUrlSchemeHandler
) )
from urllib.parse import parse_qs from urllib.parse import parse_qs
@ -343,14 +345,12 @@ def run_rapydscript_tests():
from calibre.gui2 import must_use_qt from calibre.gui2 import must_use_qt
from calibre.gui2.viewer.web_view import send_reply from calibre.gui2.viewer.web_view import send_reply
from calibre.utils.webengine import ( from calibre.utils.webengine import (
create_script, insert_scripts, secure_webengine, setup_default_profile, setup_profile create_script, insert_scripts, secure_webengine, setup_default_profile,
setup_fake_protocol, setup_profile
) )
must_use_qt() must_use_qt()
setup_default_profile() setup_default_profile()
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii')) setup_fake_protocol()
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
base = base_dir() base = base_dir()
rapydscript_dir = os.path.join(base, 'src', 'pyj') rapydscript_dir = os.path.join(base, 'src', 'pyj')

View File

@ -3,11 +3,25 @@
# License: GPL v3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import json, os import json
import os
from qt.core import QBuffer, QIODevice, QObject, pyqtSignal, sip from qt.core import QBuffer, QIODevice, QObject, pyqtSignal, sip
from qt.webengine import QWebEngineScript, QWebEngineSettings, QWebEngineProfile from qt.webengine import (
QWebEngineProfile, QWebEngineScript, QWebEngineSettings, QWebEngineUrlScheme
)
from calibre.constants import cache_dir, SPECIAL_TITLE_FOR_WEBENGINE_COMMS from calibre.constants import (
FAKE_PROTOCOL, SPECIAL_TITLE_FOR_WEBENGINE_COMMS, cache_dir
)
def setup_fake_protocol():
p = FAKE_PROTOCOL.encode('ascii')
if not QWebEngineUrlScheme.schemeByName(p).name():
scheme = QWebEngineUrlScheme(p)
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
def setup_profile(profile): def setup_profile(profile):