Open With: Load high resolution icons from exe files on windows

This commit is contained in:
Kovid Goyal 2015-03-06 15:00:22 +05:30
parent 63caf21996
commit 615a3dfcf5
2 changed files with 113 additions and 26 deletions

View File

@ -6,10 +6,9 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import os, uuid, re import os, uuid
from threading import Thread from threading import Thread
from functools import partial from functools import partial
from future_builtins import map
from PyQt5.Qt import ( from PyQt5.Qt import (
QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt, QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
@ -63,37 +62,15 @@ def entry_to_icon_text(entry, only_text=False):
if iswindows: if iswindows:
# Windows {{{ # Windows {{{
from calibre.utils.winreg.default_programs import find_programs, friendly_app_name from calibre.utils.winreg.default_programs import find_programs, friendly_app_name
from calibre.gui2 import must_use_qt from calibre.utils.open_with.windows import load_icon_resource
from win32gui import ExtractIconEx, DestroyIcon
from win32process import CreateProcess, STARTUPINFO from win32process import CreateProcess, STARTUPINFO
from win32event import WaitForInputIdle from win32event import WaitForInputIdle
import win32con import win32con
from PyQt5.Qt import QtWin
oprefs = JSONConfig('windows_open_with') oprefs = JSONConfig('windows_open_with')
def entry_sort_key(entry): def entry_sort_key(entry):
return sort_key(entry.get('name') or '') 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): def finalize_entry(entry):
data = load_icon_resource(entry.pop('icon_resource', None), as_data=True) data = load_icon_resource(entry.pop('icon_resource', None), as_data=True)
if data: if data:
@ -104,7 +81,7 @@ if iswindows:
icon = load_icon_resource(entry.get('icon_resource')) icon = load_icon_resource(entry.get('icon_resource'))
if not icon: if not icon:
icon = entry_to_icon_text(entry)[0] 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.setData(ENTRY_ROLE, entry)
ans.setToolTip(_('Command line:') + '\n' + entry['cmdline']) ans.setToolTip(_('Command line:') + '\n' + entry['cmdline'])

View File

@ -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 <kovid at kovidgoyal.net>'
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'<HHH', data, pos)
pos += 6
fmt = b'<BBBBHHIH'
ssz = struct.calcsize(fmt)
icons = []
Icon = namedtuple('Icon', 'size width height id')
while count > 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)