0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Win / Mac】アプリパスからアプリアイコンを保存する

Last updated at Posted at 2024-11-18

目標

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)

解説

割愛(リクエストがあれば書きます)

謝辞

上記のコードの中には私が書いたものだけではなく、ネットを彷徨って見つけたコードのコピペも紛れています。それらのページを見つけることができなかったので出典がありません。原作者の方ごめんなさい。
もしコピペ元だと思われるページを見つけた方は教えていただけるとありがたいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?