diff --git a/src/calibre/gui2/open_with.py b/src/calibre/gui2/open_with.py index 854331d10f..daa27952b2 100644 --- a/src/calibre/gui2/open_with.py +++ b/src/calibre/gui2/open_with.py @@ -6,10 +6,9 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' -import os, uuid, re +import os, uuid from threading import Thread from functools import partial -from future_builtins import map from PyQt5.Qt import ( QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt, @@ -63,37 +62,15 @@ def entry_to_icon_text(entry, only_text=False): if iswindows: # Windows {{{ from calibre.utils.winreg.default_programs import find_programs, friendly_app_name - from calibre.gui2 import must_use_qt - from win32gui import ExtractIconEx, DestroyIcon + from calibre.utils.open_with.windows import load_icon_resource from win32process import CreateProcess, STARTUPINFO from win32event import WaitForInputIdle import win32con - from PyQt5.Qt import QtWin oprefs = JSONConfig('windows_open_with') def entry_sort_key(entry): return sort_key(entry.get('name') or '') - def load_icon_resource(icon_resource, as_data=False): - if not icon_resource: - return - parts = tuple(filter(None, re.split(r',([-0-9]+$)', icon_resource))) - if len(parts) != 2: - return - module, index = parts - large_icons, small_icons = ExtractIconEx(module, int(index), 1) - icons = large_icons + small_icons - try: - if icons: - must_use_qt() - pixmap = QtWin.fromHICON(icons[0]) - pixmap = pixmap.scaled(48, 48, transformMode=Qt.SmoothTransformation) - if as_data: - return pixmap_to_data(pixmap) - return QIcon(pixmap) - finally: - tuple(map(DestroyIcon, icons)) - def finalize_entry(entry): data = load_icon_resource(entry.pop('icon_resource', None), as_data=True) if data: @@ -104,7 +81,7 @@ if iswindows: icon = load_icon_resource(entry.get('icon_resource')) if not icon: icon = entry_to_icon_text(entry)[0] - ans = QListWidgetItem(icon, entry.get('name') or _('Unknown'), parent) + ans = QListWidgetItem(QIcon(icon), entry.get('name') or _('Unknown'), parent) ans.setData(ENTRY_ROLE, entry) ans.setToolTip(_('Command line:') + '\n' + entry['cmdline']) diff --git a/src/calibre/utils/open_with/windows.py b/src/calibre/utils/open_with/windows.py new file mode 100644 index 0000000000..532483c559 --- /dev/null +++ b/src/calibre/utils/open_with/windows.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2015, Kovid Goyal ' + +import re, struct, ctypes +from collections import namedtuple +from future_builtins import map + +from PyQt5.Qt import QtWin, Qt, QIcon, QByteArray, QBuffer, QPixmap +import win32con, win32api, win32gui + +from calibre.gui2 import must_use_qt +ICON_SIZE = 64 + +def hicon_to_pixmap(hicon): + return QtWin.fromHICON(hicon) + +def pixmap_to_data(pixmap): + ba = QByteArray() + buf = QBuffer(ba) + buf.open(QBuffer.WriteOnly) + pixmap.save(buf, 'PNG') + return bytearray(ba.data()) + +def copy_to_size(pixmap, size=ICON_SIZE): + if pixmap.width() > ICON_SIZE: + return pixmap.scaled(ICON_SIZE, ICON_SIZE, transformMode=Qt.SmoothTransformation) + return pixmap.copy() + +def simple_load_icon(module, index, as_data=False, size=ICON_SIZE): + ' Use the win32 API ExtractIcon to load the icon. This restricts icon size to 32x32, but has less chance of failing ' + large_icons, small_icons = win32gui.ExtractIconEx(module, index, 10) + icons = large_icons + small_icons + try: + if icons: + must_use_qt() + pixmap = copy_to_size(QtWin.fromHICON(icons[0]), size=size) + if as_data: + return pixmap_to_data(pixmap) + return QIcon(pixmap) + finally: + tuple(map(win32gui.DestroyIcon, icons)) + + +def read_icon(handle, icon): + must_use_qt() + resource = win32api.LoadResource(handle, win32con.RT_ICON, icon.id) + pixmap = QPixmap() + pixmap.loadFromData(resource) + hicon = None + if pixmap.isNull(): + if icon.width > 0 and icon.height > 0: + hicon = ctypes.windll.user32.CreateIconFromResourceEx( + resource, len(resource), True, 0x00030000, icon.width, icon.height, win32con.LR_DEFAULTCOLOR) + else: + hicon = win32gui.CreateIconFromResource(resource, True) + pixmap = hicon_to_pixmap(hicon).copy() + win32gui.DestroyIcon(hicon) + return pixmap + + +def load_icon(module, index, as_data=False, size=ICON_SIZE): + handle = win32api.LoadLibraryEx(module, 0, 0x20 | 0x2) + try: + ids = win32api.EnumResourceNames(handle, win32con.RT_GROUP_ICON) + grp_id = -index if index < 0 else ids[index] + data = win32api.LoadResource(handle, win32con.RT_GROUP_ICON, grp_id) + pos = 0 + reserved, idtype, count = struct.unpack_from(b' 0: + count -= 1 + width, height, color_count, reserved, planes, bitcount, isize, icon_id = struct.unpack_from(fmt, data, pos) + icons.append(Icon(isize, width, height, icon_id)) + pos += ssz + # Unfortunately we cannot simply choose the icon with the largest + # width/height as the width and height are not recorded for PNG images + pixmaps = [] + for icon in icons: + ans = read_icon(handle, icon) + if ans is not None and not ans.isNull(): + pixmaps.append(ans) + pixmaps.sort(key=lambda p:p.size().width(), reverse=True) + pixmap = copy_to_size(pixmaps[0], size=size) + if as_data: + pixmap = pixmap_to_data(pixmap) + return pixmap + finally: + win32api.FreeLibrary(handle) + +def load_icon_resource(icon_resource, as_data=False, size=ICON_SIZE): + if not icon_resource: + return + parts = tuple(filter(None, re.split(r',([-0-9]+$)', icon_resource))) + if len(parts) != 2: + return + module, index = parts + index = int(index) + try: + return load_icon(module, index, as_data=as_data, size=size) + except Exception: + return simple_load_icon(module, index, as_data=as_data, size=size)