diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 863d1feb9a..9d4bdb6e25 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -17,7 +17,6 @@ from PyQt5.Qt import ( ) from calibre import prints -from calibre.constants import config_dir from calibre.customize.ui import available_input_formats from calibre.gui2 import choose_files, error_dialog from calibre.gui2.image_popup import ImagePopup @@ -30,14 +29,15 @@ from calibre.gui2.viewer.convert_book import prepare_book, update_book from calibre.gui2.viewer.lookup import Lookup from calibre.gui2.viewer.toc import TOC, TOCSearch, TOCView from calibre.gui2.viewer.web_view import ( - WebView, get_path_for_name, get_session_pref, set_book_path, vprefs + WebView, get_path_for_name, get_session_pref, set_book_path, viewer_config_dir, + vprefs ) from calibre.utils.date import utcnow from calibre.utils.ipc.simple_worker import WorkerError from calibre.utils.serialize import json_loads from polyglot.builtins import as_bytes, itervalues -annotations_dir = os.path.join(config_dir, 'viewer', 'annots') +annotations_dir = os.path.join(viewer_config_dir, 'annots') def dock_defs(): diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 30e0f33574..389cbfb384 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals import os +import shutil import sys from itertools import count @@ -19,11 +20,12 @@ from PyQt5.QtWebEngineWidgets import ( from calibre import as_unicode, prints from calibre.constants import ( - FAKE_HOST, FAKE_PROTOCOL, __version__, is_running_from_develop, isosx, iswindows + FAKE_HOST, FAKE_PROTOCOL, __version__, config_dir, is_running_from_develop, + isosx, iswindows ) from calibre.ebooks.metadata.book.base import field_metadata from calibre.ebooks.oeb.polish.utils import guess_type -from calibre.gui2 import error_dialog, safe_open_url +from calibre.gui2 import choose_images, error_dialog, safe_open_url from calibre.gui2.webengine import ( Bridge, RestartingWebEngineView, create_script, from_js, insert_scripts, secure_webengine, to_js @@ -31,7 +33,7 @@ from calibre.gui2.webengine import ( from calibre.srv.code import get_translations_data from calibre.utils.config import JSONConfig from calibre.utils.serialize import json_loads -from polyglot.builtins import iteritems +from polyglot.builtins import as_bytes, iteritems try: from PyQt5 import sip @@ -39,6 +41,7 @@ except ImportError: import sip vprefs = JSONConfig('viewer-webengine') +viewer_config_dir = os.path.join(config_dir, 'viewer') vprefs.defaults['session_data'] = {} vprefs.defaults['main_window_state'] = None vprefs.defaults['main_window_geometry'] = None @@ -77,6 +80,20 @@ def get_data(name): return None, None +def background_image(): + ans = getattr(background_image, 'ans', None) + if ans is None: + img_path = os.path.join(viewer_config_dir, 'bg-image.data') + if os.path.exists(img_path): + with open(img_path, 'rb') as f: + data = f.read() + mt, data = data.split(b'|', 1) + else: + ans = b'image/jpeg', b'' + ans = background_image.ans = mt.decode('utf-8'), data + return ans + + def send_reply(rq, mime_type, data): if sip.isdeleted(rq): return @@ -131,6 +148,12 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler): elif name == 'manifest': data = b'[' + set_book_path.manifest + b',' + set_book_path.metadata + b']' send_reply(rq, set_book_path.manifest_mime, data) + elif name == 'reader-background': + mt, data = background_image() + if data: + send_reply(rq, mt, data) + else: + rq.fail(rq.UrlNotFound) elif name.startswith('mathjax/'): from calibre.gui2.viewer.mathjax import monkeypatch_mathjax if name == 'mathjax/manifest.json': @@ -206,6 +229,7 @@ class ViewerBridge(Bridge): selection_changed = from_js(object) copy_selection = from_js(object) view_image = from_js(object) + change_background_image = from_js(object) create_view = to_js() show_preparing_message = to_js() @@ -215,6 +239,7 @@ class ViewerBridge(Bridge): full_screen_state_changed = to_js() get_current_cfi = to_js() show_home_page = to_js() + background_image_changed = to_js() def apply_font_settings(page_or_view): @@ -369,6 +394,7 @@ class WebView(RestartingWebEngineView): self.bridge.selection_changed.connect(self.selection_changed) self.bridge.view_image.connect(self.view_image) self.bridge.report_cfi.connect(self.call_callback) + self.bridge.change_background_image.connect(self.change_background_image) self.pending_bridge_ready_actions = {} self.setPage(self._page) self.setAcceptDrops(False) @@ -483,3 +509,13 @@ class WebView(RestartingWebEngineView): def show_home_page(self): self.execute_when_ready('show_home_page') + + def change_background_image(self, img_id): + files = choose_images(self, 'viewer-background-image', _('Choose background image'), formats=['png', 'gif', 'jpg', 'jpeg']) + if files: + img = files[0] + with open(img, 'rb') as src, open(os.path.join(viewer_config_dir, 'bg-image.data'), 'wb') as dest: + dest.write(as_bytes(guess_type(img)[0] or 'image/jpeg') + b'|') + shutil.copyfileobj(src, dest) + background_image.ans = None + self.execute_when_ready('background_image_changed', img_id) diff --git a/src/pyj/read_book/prefs/main.pyj b/src/pyj/read_book/prefs/main.pyj index 836a3805e9..b9b2e4cb4d 100644 --- a/src/pyj/read_book/prefs/main.pyj +++ b/src/pyj/read_book/prefs/main.pyj @@ -69,7 +69,7 @@ class Prefs: items = [ create_item(_('Colors'), def():self.show_panel('colors');, _('Colors of the page and text')), create_item(_('Page layout'), def():self.show_panel('layout');, _('Page margins and number of pages per screen')), - create_item(_('User style sheet'), def():self.show_panel('user_stylesheet');, _('Style rules for text')), + create_item(_('Styles'), def():self.show_panel('user_stylesheet');, _('Style rules for text and background image')), create_item(_('Headers and footers'), def():self.show_panel('head_foot');, _('Customize the headers and footers')), create_item(_('Keyboard shortcuts'), def():self.show_panel('keyboard');, _('Customize the keyboard shortcuts')), ] @@ -109,7 +109,7 @@ class Prefs: commit_layout(self.onchange, self.container) def display_user_stylesheet(self, container): - document.getElementById(self.title_id).textContent = _('User style sheet') + document.getElementById(self.title_id).textContent = _('Styles') create_user_stylesheet_panel(container) def close_user_stylesheet(self): diff --git a/src/pyj/read_book/prefs/user_stylesheet.pyj b/src/pyj/read_book/prefs/user_stylesheet.pyj index 3e85a17c66..2d8631e77b 100644 --- a/src/pyj/read_book/prefs/user_stylesheet.pyj +++ b/src/pyj/read_book/prefs/user_stylesheet.pyj @@ -6,18 +6,54 @@ from elementmaker import E from gettext import gettext as _ from book_list.globals import get_session_data -from read_book.globals import runtime +from read_book.globals import runtime, ui_operations +from viewer.constants import READER_BACKGROUND_URL +from widgets import create_button +from dom import unique_id + + +BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' + + +def change_background_image(img_id): + ui_operations.change_background_image(img_id) + + +def clear_image(img_id): + document.getElementById(img_id).src = BLANK + + +def background_widget(sd): + if sd.get('background_image'): + src = READER_BACKGROUND_URL + else: + src = BLANK + img_id = unique_id('bg-image') + + return E.div( + style='display: flex; align-items: center', + E.div(E.img(src=src, id=img_id, class_='bg-image-preview', style='width: 75px; height: 75px; border: solid 1px')), + E.div('\xa0', style='margin: 0.5rem'), + create_button(_('Change image'), action=change_background_image.bind(None, img_id)), + E.div('\xa0', style='margin: 0.5rem'), + create_button(_('Clear image'), action=clear_image.bind(None, img_id)), + ) def create_user_stylesheet_panel(container): sd = get_session_data() container.appendChild( E.div( - style='min-height: 80vh; display: flex; flex-flow: column; margin: 1ex 1rem; padding: 1ex 0', + style='min-height: 75vh; display: flex; flex-flow: column; margin: 1ex 1rem; padding: 1ex 0', + E.div( + style='border-bottom: solid 1px; margin-bottom: 1.5ex; padding-bottom: 1.5ex', + E.div(_('Choose a background image to display behind the book text'), style='margin-bottom: 1.5ex'), + background_widget(sd), + ), E.div( style='flex-grow: 10; display: flex; flex-flow: column', E.div( - _('A CSS style sheet that can be used to control the look and feel of books. For examples, click'), ' ', + _('A CSS style sheet that can be used to control the look and feel of the text. For examples, click'), ' ', E.a(class_='blue-link', title=_("Examples of user style sheets"), target=('_self' if runtime.is_standalone_viewer else '_blank'), href='https://www.mobileread.com/forums/showthread.php?t=51500', _('here')) @@ -38,6 +74,16 @@ def commit_user_stylesheet(onchange, container): ta = container.querySelector('[name=user-stylesheet]') val = ta.value or '' old = sd.get('user_stylesheet') + changed = False if old is not val: sd.set('user_stylesheet', val) + changed = True + bg_image = container.querySelector('img.bg-image-preview').src + if bg_image is BLANK: + bg_image = None + old = sd.get('background_image') + if old is not bg_image: + sd.set('background_image', bg_image) + changed = True + if changed: onchange() diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index b5bff4b98b..8503888f20 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -29,7 +29,10 @@ from read_book.timers import Timers from read_book.toc import get_current_toc_nodes, update_visible_toc_nodes from read_book.touch import set_left_margin_handler, set_right_margin_handler from session import get_device_uuid, get_interface_data -from utils import html_escape, is_ios, parse_url_params, username_key, safe_set_inner_html +from utils import ( + html_escape, is_ios, parse_url_params, safe_set_inner_html, username_key +) +from viewer.constants import READER_BACKGROUND_URL add_extra_css(def(): sel = '.book-side-margin' @@ -435,7 +438,17 @@ class View: s = m.style s.color = ans.foreground s.backgroundColor = ans.background + sd = get_session_data() self.iframe.style.backgroundColor = ans.background or 'white' + bg_image = sd.get('background_image') + if bg_image: + if runtime.is_standalone_viewer: + self.iframe.style.backgroundImage = f'url({READER_BACKGROUND_URL})' + else: + self.iframe.style.backgroundImage = f'url({bg_image})' + else: + self.iframe.style.backgroundImage = 'none' + m.parentNode.style.backgroundColor = ans.background # this is needed on iOS where the bottom margin has its own margin, so we dont want the body background color to bleed through self.content_popup_overlay.apply_color_scheme(ans.background, ans.foreground) return ans diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index 853b14f02c..bc4f39b2a5 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -34,6 +34,8 @@ defaults = { 'max_text_width': 0, 'columns_per_screen': {'portrait':0, 'landscape':0}, 'user_stylesheet': '', + 'background_image': None, + 'background_image_style': 'stretch', 'current_color_scheme': 'white', 'user_color_schemes': {}, 'base_font_size': 16, @@ -58,6 +60,8 @@ is_local_setting = { 'max_text_width': True, 'columns_per_screen': True, 'user_stylesheet': True, + 'background_image': True, + 'background_image_style': True, 'current_color_scheme': True, 'base_font_size': True, 'controls_help_shown_count': True, diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index b63e8a1908..f7fb8fc826 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -22,7 +22,7 @@ from read_book.shortcuts import add_standalone_viewer_shortcuts from read_book.view import View from session import session_defaults from utils import encode_query_with_path, parse_url_params -from viewer.constants import FAKE_HOST, FAKE_PROTOCOL +from viewer.constants import FAKE_HOST, FAKE_PROTOCOL, READER_BACKGROUND_URL runtime.is_standalone_viewer = True runtime.FAKE_HOST = FAKE_HOST @@ -237,6 +237,14 @@ def get_current_cfi(request_id): view.get_current_cfi(request_id, ui_operations.report_cfi) +@from_python +def background_image_changed(img_id): + img = document.getElementById(img_id) + if img: + img.src = '' + img.src = READER_BACKGROUND_URL + + def onerror(msg, script_url, line_number, column_number, error_object): if not error_object: # cross domain error @@ -295,6 +303,8 @@ if window is window.top: to_python.copy_selection(text or None) ui_operations.view_image = def(name): to_python.view_image(name) + ui_operations.change_background_image = def(img_id): + to_python.change_background_image(img_id) document.body.appendChild(E.div(id='view')) window.onerror = onerror diff --git a/src/pyj/viewer/constants.pyj b/src/pyj/viewer/constants.pyj index aaabf5aefe..5ee296b986 100644 --- a/src/pyj/viewer/constants.pyj +++ b/src/pyj/viewer/constants.pyj @@ -5,3 +5,4 @@ from __python__ import bound_methods, hash_literals FAKE_PROTOCOL = '__FAKE_PROTOCOL__' FAKE_HOST = '__FAKE_HOST__' +READER_BACKGROUND_URL = f'{FAKE_PROTOCOL}://{FAKE_HOST}/reader-background'