From 90a0a33723be64bb442fd1064ffa78e465b537b9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 4 Feb 2024 09:18:30 +0530 Subject: [PATCH] Windows: Fix a regression in 7.0 that caused images referring to files on the disk within comments columns to not display in some circumstances --- src/calibre/gui2/__init__.py | 20 ++++++++++ src/calibre/gui2/book_details.py | 6 +++ src/calibre/gui2/comments_editor.py | 48 ++++++++++++------------ src/calibre/gui2/dialogs/book_info.py | 3 ++ src/calibre/gui2/widgets2.py | 54 ++++++++++++++++----------- 5 files changed, 84 insertions(+), 47 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 21c8f18bc0..f18bd8fabf 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -1680,3 +1680,23 @@ def timed_print(*a, **kw): if not hasattr(timed_print, 'startup_time'): timed_print.startup_time = monotonic() print(f'[{monotonic() - timed_print.startup_time:.2f}]', *a, **kw) + + +def local_path_for_resource(qurl: QUrl, base_qurl: 'QUrl | None' = None) -> str: + import re + + def fix_qt_bodging_windows_paths(path: str) -> str: + # When loading Qt gives us the + # URL: //c/path/to/img.png Le bubbling sigh + if iswindows and re.match(r'//[a-zA-Z]/', path) is not None and not os.path.exists(path): + path = os.path.normpath(path[2] + ':' + path[3:]) + return path + + if base_qurl and qurl.isRelative(): + qurl = base_qurl.resolved(qurl) + + if qurl.isLocalFile(): + return fix_qt_bodging_windows_paths(qurl.toLocalFile()) + if qurl.isRelative(): # this means has no scheme + return fix_qt_bodging_windows_paths(qurl.path()) + return '' diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 0723b50359..8d18a38e2f 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -998,6 +998,7 @@ class BookInfo(HTMLDisplay): HTMLDisplay.__init__(self, parent=parent, save_resources_in_document=False) self.vertical = vertical self.last_rendered_html = '', '', '' + self.base_url_for_current_book = None self.anchor_clicked.connect(self.link_activated) for x, icon in [ ('remove_format', 'trash.png'), ('save_format', 'save.png'), @@ -1088,8 +1089,13 @@ class BookInfo(HTMLDisplay): def show_data(self, mi): html, table, comments = self.last_rendered_html = render_html(mi, self.vertical, self.parent()) + path = getattr(mi, 'path', None) + self.base_url_for_current_book = QUrl.fromLocalFile(os.path.join(path, 'metadata.opf')) if path else None set_html(mi, html, self) + def get_base_qurl(self): + return self.base_url_for_current_book + def process_external_css(self, css): return resolve_colors(css) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 1fb889c31d..153376f561 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -7,7 +7,6 @@ import re import sys import weakref from collections import defaultdict -from threading import Thread from contextlib import contextmanager from functools import partial from html5_parser import parse @@ -21,15 +20,16 @@ from qt.core import ( QTextFrameFormat, QTextImageFormat, QTextListFormat, QTimer, QToolButton, QUrl, QVBoxLayout, QWidget, pyqtSignal, pyqtSlot, ) +from threading import Thread from calibre import browser, fit_image, xml_replace_entities +from calibre.constants import iswindows from calibre.db.constants import DATA_DIR_NAME from calibre.ebooks.chardet import xml_to_unicode from calibre.gui2 import ( - NO_URL_FORMATTING, choose_dir, choose_files, error_dialog, gprefs, is_dark_theme, - question_dialog, safe_open_url, + NO_URL_FORMATTING, FunctionDispatcher, choose_dir, choose_files, error_dialog, + gprefs, is_dark_theme, local_path_for_resource, question_dialog, safe_open_url, ) -from calibre.gui2 import FunctionDispatcher from calibre.gui2.book_details import resolved_css from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.flow_toolbar import create_flow_toolbar @@ -38,6 +38,7 @@ from calibre.gui2.widgets2 import to_plain_text from calibre.startup import connect_lambda from calibre.utils.cleantext import clean_xml_chars from calibre.utils.config import tweaks +from calibre.utils.filenames import make_long_path_useable from calibre.utils.imghdr import what from polyglot.builtins import iteritems, itervalues @@ -929,27 +930,24 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{ @pyqtSlot(int, 'QUrl', result='QVariant') def loadResource(self, rtype, qurl): - if self.base_url: - if qurl.isRelative(): - qurl = self.base_url.resolved(qurl) - if qurl.isLocalFile(): - data = None - path = qurl.toLocalFile() - try: - with open(path, 'rb') as f: - data = f.read() - except OSError: - if path.rpartition('.')[-1].lower() in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: - data = bytearray.fromhex( - '89504e470d0a1a0a0000000d49484452' - '000000010000000108060000001f15c4' - '890000000a49444154789c6300010000' - '0500010d0a2db40000000049454e44ae' - '426082') - if data is not None: - r = QByteArray(data) - self.document().addResource(rtype, qurl, r) - return r + path = local_path_for_resource(qurl, base_qurl=self.base_url) + if path: + data = None + try: + with open(make_long_path_useable(path), 'rb') as f: + data = f.read() + except OSError: + if path.rpartition('.')[-1].lower() in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: + data = bytearray.fromhex( + '89504e470d0a1a0a0000000d49484452' + '000000010000000108060000001f15c4' + '890000000a49444154789c6300010000' + '0500010d0a2db40000000049454e44ae' + '426082') + if data is not None: + r = QByteArray(data) + self.document().addResource(rtype, qurl, r) + return r def set_html(self, val, allow_undo=True): if not allow_undo or self.readonly: diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 2af465b97d..9c48d040e1 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -129,6 +129,9 @@ class Details(HTMLDisplay): self.allow_context_menu = allow_context_menu self.is_locked = is_locked + def get_base_qurl(self): + return getattr(self.book_info, 'base_url_for_current_book', None) + def sizeHint(self): return QSize(350, 350) diff --git a/src/calibre/gui2/widgets2.py b/src/calibre/gui2/widgets2.py index 47f24df08a..5121803498 100644 --- a/src/calibre/gui2/widgets2.py +++ b/src/calibre/gui2/widgets2.py @@ -16,7 +16,9 @@ from qt.core import ( from calibre import prepare_string_for_xml from calibre.constants import builtin_colors_dark, builtin_colors_light from calibre.ebooks.metadata import rating_to_stars -from calibre.gui2 import UNDEFINED_QDATETIME, gprefs, rating_font +from calibre.gui2 import ( + UNDEFINED_QDATETIME, gprefs, local_path_for_resource, rating_font, +) from calibre.gui2.complete2 import EditWithComplete, LineEdit from calibre.gui2.widgets import history from calibre.utils.config_base import tweaks @@ -543,6 +545,9 @@ class HTMLDisplay(QTextBrowser): self.setAcceptDrops(False) self.anchorClicked.connect(self.on_anchor_clicked) + def get_base_qurl(self): + return None + def setHtml(self, html): self.last_set_html = html QTextBrowser.setHtml(self, html) @@ -573,29 +578,34 @@ class HTMLDisplay(QTextBrowser): return self.anchor_clicked.emit(qurl) - def loadResource(self, rtype, qurl): - if qurl.isLocalFile(): - path = qurl.toLocalFile() - try: - with open(path, 'rb') as f: - data = f.read() - except OSError: - if path.rpartition('.')[-1].lower() in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: - r = QByteArray(bytearray.fromhex( - '89504e470d0a1a0a0000000d49484452' - '000000010000000108060000001f15c4' - '890000000a49444154789c6300010000' - '0500010d0a2db40000000049454e44ae' - '426082')) - if self.save_resources_in_document: - self.document().addResource(rtype, qurl, r) - return r - else: - r = QByteArray(data) + def load_local_file_resource(self, rtype, qurl, path): + from calibre.utils.filenames import make_long_path_useable + try: + with open(make_long_path_useable(path), 'rb') as f: + data = f.read() + except OSError: + if path.rpartition('.')[-1].lower() in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: + r = QByteArray(bytearray.fromhex( + '89504e470d0a1a0a0000000d49484452' + '000000010000000108060000001f15c4' + '890000000a49444154789c6300010000' + '0500010d0a2db40000000049454e44ae' + '426082')) if self.save_resources_in_document: self.document().addResource(rtype, qurl, r) return r - elif qurl.scheme() == 'calibre-icon': + else: + r = QByteArray(data) + if self.save_resources_in_document: + self.document().addResource(rtype, qurl, r) + return r + return super().loadResource(rtype, qurl) + + def loadResource(self, rtype, qurl): + path = local_path_for_resource(qurl, base_qurl=self.get_base_qurl()) + if path: + return self.load_local_file_resource(rtype, qurl, path) + if qurl.scheme() == 'calibre-icon': r = QIcon.icon_as_png(qurl.path().lstrip('/'), as_bytearray=True) self.document().addResource(rtype, qurl, r) return r @@ -611,7 +621,7 @@ class HTMLDisplay(QTextBrowser): self.document().addResource(rtype, qurl, r) return r else: - return QTextBrowser.loadResource(self, rtype, qurl) + return super().loadResource(rtype, qurl) def anchorAt(self, pos): # Anchors in a document can be "focused" with the tab key.