diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py index e14b82eda2..20645a385e 100644 --- a/src/calibre/customize/zipplugin.py +++ b/src/calibre/customize/zipplugin.py @@ -13,6 +13,7 @@ import sys import threading import zipfile from collections import OrderedDict +from collections.abc import Iterable from functools import partial from importlib.machinery import ModuleSpec from importlib.util import decode_source @@ -54,7 +55,11 @@ def get_resources(zfp, name_or_list_of_names, print_tracebacks_for_missing_resou return ans -def get_icons(zfp, name_or_list_of_names, plugin_name='', print_tracebacks_for_missing_resources=True): +def get_icons( + zfp: str, name_or_list_of_names: str | Iterable[str], + plugin_name: str = '', folder_in_zip_file: str = '', + print_tracebacks_for_missing_resources: bool = True, +): ''' Load icons from the plugin zip file @@ -64,41 +69,34 @@ def get_icons(zfp, name_or_list_of_names, plugin_name='', print_tracebacks_for_m :param plugin_name: The human friendly name of the plugin, used to load icons from the current theme, if present. + :param folder_in_zip_file: Path to a folder in the zip file from which to load the icons. + Default is the root of the zip file. Use / as separator. + :param print_tracebacks_for_missing_resources: When True missing resources are reported to STDERR :return: A dictionary of the form ``{name : QIcon}``. Any names that were not found in the zip file will be null QIcons. If a single path is passed in the return value will - be A QIcon. + be a QIcon. ''' - from qt.core import QIcon, QPixmap + from qt.core import QIcon + namelist = (name_or_list_of_names,) if isinstance(name_or_list_of_names, (str, bytes)) else name_or_list_of_names ans = {} - namelist = [name_or_list_of_names] if isinstance(name_or_list_of_names, (str, bytes)) else name_or_list_of_names - failed = set() - if plugin_name: + with zipfile.ZipFile(zfp) as zf: for name in namelist: - q = QIcon.ic(f'{plugin_name}/{name}') - if q.is_ok(): - ans[name] = q - else: - failed.add(name) - else: - failed = set(namelist) - if failed: - from_zfp = get_resources(zfp, list(failed), print_tracebacks_for_missing_resources=print_tracebacks_for_missing_resources) - if from_zfp is None: - from_zfp = {} - elif isinstance(from_zfp, (str, bytes)): - from_zfp = {namelist[0]: from_zfp} - - for name in failed: - p = QPixmap() - raw = from_zfp.get(name) - if raw: - p.loadFromData(raw) - ans[name] = QIcon(p) - if len(namelist) == 1 and ans: - ans = ans.pop(namelist[0]) + arcname = posixpath.join(folder_in_zip_file, name) + theme_name = posixpath.join(plugin_name, name) + try: + data = zf.read(arcname) + except KeyError: + data = b'' + if print_tracebacks_for_missing_resources: + print('Failed to load resource:', repr(name), 'from the plugin zip file:', zfp, file=sys.stderr) + import traceback + traceback.print_exc() + ans[name] = QIcon.ic(theme_name, fallback=data) + if len(namelist) == 1: + return ans[name] return ans diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 255c2822a6..4f9abf5265 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -41,6 +41,7 @@ from qt.core import ( QObject, QPainterPath, QPalette, + QPixmap, QRectF, QResource, QSettings, @@ -251,7 +252,7 @@ class IconResourceManager: icon = self.icon_cache[name] = self(name) return icon - def __call__(self, name): + def __call__(self, name: str, fallback: bytes = b'') -> QIcon: if isinstance(name, QIcon): return name if not name: @@ -269,6 +270,11 @@ class IconResourceManager: q = QIcon(f':/icons/calibre-default-{self.color_palette}/images/{name}') if q.is_ok(): ans = q + if fallback and not ans.is_ok(): + p = QPixmap() + p.loadFromData(fallback) + if not p.isNull(): + ans = QIcon(p) return ans def icon_as_png(self, name, as_bytearray=False, compression_level=0):