目標
save_app_icon("アプリファイル(.exeまたは.app)へのパス", "保存先パス(.png)")
環境構築
Windows
pip install pillow
Mac
pip install pyobjc
まとめてインストールするにはrequirements.txtを作成します。
requirements.txt
pyobjc; sys_platform == 'darwin'
pillow; sys_platform == 'win32'
pip install -r requirements.txt
コード
import sys
if sys.platform == "darwin":
from AppKit import NSWorkspace, NSBitmapImageRep, NSBitmapImageFileTypePNG
def save_app_icon(app_path: str, file_path: str) -> None:
icon = NSWorkspace.sharedWorkspace().iconForFile_(app_path)
bitmap = NSBitmapImageRep(data=icon.TIFFRepresentation())
bitmap.representationUsingType_properties_(NSBitmapImageFileTypePNG, None).writeToFile_atomically_(file_path, False)
elif sys.platform == "win32":
import ctypes
from enum import Enum
from math import sqrt
from decimal import Decimal
from PIL import Image
from ctypes.wintypes import BOOL, BYTE, DWORD, HBITMAP, HDC, HICON, HGDIOBJ, LONG, LPCWSTR, LPVOID, UINT, WORD
BI_RGB = 0
DIB_RGB_COLORS = 0
class ICONINFO(ctypes.Structure):
_fields_ = [
("fIcon", BOOL),
("xHotspot", DWORD),
("yHotspot", DWORD),
("hbmMask", HBITMAP),
("hbmColor", HBITMAP)
]
class RGBQUAD(ctypes.Structure):
_fields_ = [
("rgbBlue", BYTE),
("rgbGreen", BYTE),
("rgbRed", BYTE),
("rgbReserved", BYTE),
]
class BITMAPINFOHEADER(ctypes.Structure):
_fields_ = [
("biSize", DWORD),
("biWidth", LONG),
("biHeight", LONG),
("biPlanes", WORD),
("biBitCount", WORD),
("biCompression", DWORD),
("biSizeImage", DWORD),
("biXPelsPerMeter", LONG),
("biYPelsPerMeter", LONG),
("biClrUsed", DWORD),
("biClrImportant", DWORD)
]
class BITMAPINFO(ctypes.Structure):
_fields_ = [
("bmiHeader", BITMAPINFOHEADER),
("bmiColors", RGBQUAD * 1),
]
shell32 = ctypes.WinDLL("shell32", use_last_error=True)
user32 = ctypes.WinDLL("user32", use_last_error=True)
gdi32 = ctypes.WinDLL("gdi32", use_last_error=True)
gdi32.CreateCompatibleDC.argtypes = [HDC]
gdi32.CreateCompatibleDC.restype = HDC
gdi32.GetDIBits.argtypes = [
HDC, HBITMAP, UINT, UINT, LPVOID, ctypes.c_void_p, UINT
]
gdi32.GetDIBits.restype = ctypes.c_int
gdi32.DeleteObject.argtypes = [HGDIOBJ]
gdi32.DeleteObject.restype = BOOL
shell32.ExtractIconExW.argtypes = [
LPCWSTR, ctypes.c_int, ctypes.POINTER(HICON), ctypes.POINTER(HICON), UINT
]
shell32.ExtractIconExW.restype = UINT
user32.GetIconInfo.argtypes = [HICON, ctypes.POINTER(ICONINFO)]
user32.GetIconInfo.restype = BOOL
user32.DestroyIcon.argtypes = [HICON]
user32.DestroyIcon.restype = BOOL
class IconSize(Enum):
SMALL = 1
LARGE = 2
@staticmethod
def to_wh(size: "IconSize") -> tuple[int, int]:
size_table = {
IconSize.SMALL: (16, 16),
IconSize.LARGE: (32, 32)
}
return size_table[size]
def extract_icon(filename: str, size: IconSize=IconSize.LARGE) -> ctypes.Array[ctypes.c_char]:
dc: HDC = gdi32.CreateCompatibleDC(0)
if dc == 0:
raise ctypes.WinError()
hicon: HICON = HICON()
extracted_icons: UINT = shell32.ExtractIconExW(
filename,
0,
ctypes.byref(hicon) if size == IconSize.LARGE else None,
ctypes.byref(hicon) if size == IconSize.SMALL else None,
1
)
if extracted_icons != 1:
raise ctypes.WinError()
def cleanup() -> None:
if icon_info.hbmColor != 0:
gdi32.DeleteObject(icon_info.hbmColor)
if icon_info.hbmMask != 0:
gdi32.DeleteObject(icon_info.hbmMask)
user32.DestroyIcon(hicon)
icon_info: ICONINFO = ICONINFO(0, 0, 0, 0, 0)
if not user32.GetIconInfo(hicon, ctypes.byref(icon_info)):
cleanup()
raise ctypes.WinError()
w, h = icon_info.xHotspot * 2, icon_info.yHotspot * 2
bmi: BITMAPINFO = BITMAPINFO()
ctypes.memset(ctypes.byref(bmi), 0, ctypes.sizeof(bmi))
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = w
bmi.bmiHeader.biHeight = -h
bmi.bmiHeader.biPlanes = 1
bmi.bmiHeader.biBitCount = 32
bmi.bmiHeader.biCompression = BI_RGB
bmi.bmiHeader.biSizeImage = w * h * 4
bits = ctypes.create_string_buffer(bmi.bmiHeader.biSizeImage)
copied_lines = gdi32.GetDIBits(
dc, icon_info.hbmColor, 0, h, bits, ctypes.byref(bmi), DIB_RGB_COLORS
)
if copied_lines == 0:
cleanup()
raise ctypes.WinError()
cleanup()
return bits
def win32_icon_to_image(icon_bits: ctypes.Array[ctypes.c_char], size: IconSize=IconSize.LARGE) -> Image:
try:
root = sqrt(len(icon_bits))
if not Decimal(str(root)).as_tuple().exponent == -1:
w, h = IconSize.to_wh(size)
else:
w = h = int(root) // 2
img = Image.frombytes("RGBA", (w, h), icon_bits, "raw", "BGRA")
except ValueError:
w, h = IconSize.to_wh(size)
img = Image.frombytes("RGBA", (w, h), icon_bits, "raw", "BGRA")
return img
def save_app_icon(app_path: str, file_path: str) -> None:
icon_bits = extract_icon(app_path)
img = win32_icon_to_image(icon_bits)
img.save(file_path)
解説
割愛(リクエストがあれば書きます)
謝辞
上記のコードの中には私が書いたものだけではなく、ネットを彷徨って見つけたコードのコピペも紛れています。それらのページを見つけることができなかったので出典がありません。原作者の方ごめんなさい。
もしコピペ元だと思われるページを見つけた方は教えていただけるとありがたいです。