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)
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
with open(path_to_html, 'rb') as f:
raw = f.read()
@ -108,11 +108,11 @@ def render_html_svg_workaround(path_to_html, log, width=590, height=750):
pass
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
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.utils.ipc.simple_worker import fork_job, WorkerError
result = {}
@ -127,7 +127,7 @@ def render_html_data(path_to_html, width, height):
with TemporaryDirectory('-render-html') as tdir:
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:
report_error(e.orig_tb)
else:

View File

@ -218,7 +218,7 @@ class EPUBInput(InputFormatPlugin):
elem[0].getparent(), OPF('item'), href=guide_elem.get('href'), id='calibre_raster_cover')
t.set('media-type', 'image/jpeg')
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:
with lopen('calibre_raster_cover.jpg', 'wb') as f:
f.write(renderer)

View File

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

View File

@ -193,7 +193,7 @@ def render_cover(cpage, zf, reader=None):
cpage = os.path.join(tdir, cpage)
if not os.path.exists(cpage):
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):

View File

@ -625,7 +625,7 @@ class OEBReader:
writer = OEBWriter()
writer(self.oeb, tdir)
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:
data = b''
id, href = self.oeb.manifest.generate('cover', 'cover.jpg')

View File

@ -4,26 +4,88 @@
import os
import sys
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.oeb.polish.utils import guess_type
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.logging import default_log
from calibre.utils.monotonic import monotonic
from calibre.utils.webengine import (
secure_webengine, send_reply, setup_fake_protocol, setup_profile
)
LOAD_TIMEOUT = 20
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):
def __init__(self):
QWebEnginePage.__init__(self)
def __init__(self, profile):
QWebEnginePage.__init__(self, profile, QApplication.instance())
secure_webengine(self)
self.printing_started = False
self.loadFinished.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection)
@ -32,6 +94,11 @@ class Render(QWebEnginePage):
t.setInterval(500)
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):
if ok:
self.runJavaScript('''
@ -52,8 +119,10 @@ class Render(QWebEnginePage):
def javaScriptConsoleMessage(self, level, msg, linenumber, source_id):
pass
def start_load(self, path_to_html):
self.load(QUrl.fromLocalFile(path_to_html))
def start_load(self, path_to_html, root):
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.hang_timer.start()
@ -79,7 +148,9 @@ class Render(QWebEnginePage):
if type(getattr(QPageSize, sz, None)) is type(QPageSize.PageSizeId.A4): # noqa
page_size = QPageSize(getattr(QPageSize, sz))
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'))
if ps is not None:
page_size = ps
@ -95,17 +166,25 @@ class Render(QWebEnginePage):
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'):
raise ValueError('Image format must be either jpeg or png')
must_use_qt()
from calibre.utils.webengine import setup_default_profile
setup_default_profile()
setup_qt_logging()
setup_fake_protocol()
profile = setup_profile(QWebEngineProfile(QApplication.instance()))
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)
renderer = Render()
renderer.start_load(path_to_html)
renderer = Render(profile)
renderer.start_load(path_to_html, url_handler.root)
ret = QApplication.instance().exec()
renderer.break_cycles()
del renderer
if ret == 0:
page_images('rendered.pdf', 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
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_fake_protocol()
d = TOCEditor(path, title=title, write_result_to=path + '.result')
d.start()
ok = 0

View File

@ -8,7 +8,7 @@ import time
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.gui2 import (
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):
from qt.webengine import QWebEngineUrlScheme
from calibre.utils.webengine import setup_fake_protocol
# Ensure we can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir()
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii'))
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
setup_fake_protocol()
# The following two lines are needed to prevent circular imports causing
# errors during initialization of plugins that use the polish container

View File

@ -8,7 +8,7 @@ import sys
from contextlib import closing
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.listener import send_message_in_process
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):
from qt.webengine import QWebEngineUrlScheme
from calibre.utils.webengine import setup_fake_protocol
# Ensure viewer can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir()
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii'))
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
setup_fake_protocol()
override = 'calibre-ebook-viewer' if islinux else None
processed_args = []
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):
from qt.core import QLoggingCategory, QUrl
QLoggingCategory.setFilterRules('''\
qt.webenginecontext.info=false
''')
from qt.core import QUrl
from calibre.gui2 import must_use_qt
from calibre.gui_launch import setup_qt_logging
setup_qt_logging()
from .simple_backend import SimpleScraper
must_use_qt()

View File

@ -59,7 +59,9 @@ def compiler():
from calibre import walk
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()
setup_default_profile()
@ -335,7 +337,7 @@ def run_rapydscript_tests():
from qt.core import QApplication, QByteArray, QEventLoop, QUrl
from qt.webengine import (
QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineUrlRequestJob,
QWebEngineUrlScheme, QWebEngineUrlSchemeHandler
QWebEngineUrlSchemeHandler
)
from urllib.parse import parse_qs
@ -343,14 +345,12 @@ def run_rapydscript_tests():
from calibre.gui2 import must_use_qt
from calibre.gui2.viewer.web_view import send_reply
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()
setup_default_profile()
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii'))
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)
scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
setup_fake_protocol()
base = base_dir()
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>
import json, os
import json
import os
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):