From 0da6219b4c7adfd48a4b76fee71f115d1427e062 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2026 10:36:32 +0530 Subject: [PATCH] Allow plugins to specify a folder in the zip file to load icons from kiwidude's plugins use some crazy inefficient code that tries to load an icon in multiple ways. With this change he can simply do: get_icons(zfp, name, plugin_name, folder_in_zip_file='images') This will load the icon efficiently from the user overrides/icon theme, falling back to the icon from the zip file as a last resort. Also, change get_icons() to always read the icon data from the zip file. This is to support upcoming code to change icon themeing to support the case of switching from one them to another during and when the first theme has the icon and the second theme doesnt, loading the fallback/default icon. --- src/calibre/customize/zipplugin.py | 54 ++++++++++++++---------------- src/calibre/gui2/__init__.py | 8 ++++- 2 files changed, 33 insertions(+), 29 deletions(-) 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):