pythonpython-3.xpython-imaging-libraryicons.ico

How can I save the 32x32 icon from a .ico file that has the highest color depth possible as a .png using Pillow?


I'm trying to extract and save the 32x32 icon from a .ico file that contains multiple icons with multiple sizes and color depths using Pillow. The 32x32 icon is available in the following color depths: 32-bit, 8-bit and 4-bit.

I tried opening the icon file using Image.open(), then set its size to 32x32, and then save it as a .png file. I expect to get the icon with the highest color depth possible, but I'm getting the one with the lowest color depth possible.

Here's a minimal, reproducible example:

from PIL import Image

icon = Image.open("icon.ico")
icon.size = (32, 32)
icon.save("icon.png")

I'm expecting to get this: The 32x32 icon with the 32-bit color depth

But, I'm getting this: The 32x32 icon with the 4-bit color depth

You can get the .ico file here: https://www.mediafire.com/file/uls693wvjn3njqa/icon.ico/file

I also tried looking for similar questions on the internet, but I didn't find anything.

Is there a way I can tell Pillow to extract the 32x32 icon from a .ico file at the highest color depth possible without modifying my .ico file to exclude icons with lower color depths?


Solution

  • WHile an ICO image loaded in PIL provide access to the individual images through the img.ico.frame call, there is one problem: upon loading, PIL will convert all loaded individual frames which are indexed into the RGBA format.

    Fortunately, it will preserve a bpp attribute listing the original bitcount for each frame in a headers list stored in the img.ico.entry attribute.

    Which means a small function which iterates the frame headers can find the needed information:

    from PIL import Image
    
    def get_max_bpp(ico, size=(32,32)):
        frames = [(index, header, ico.ico.frame(index))
            for index, header in enumerate(icon.ico.entry) 
            if header.width==size[0] and header.height == size[1]
        ] 
        frames.sort(key=lambda frame_info: frame_info[1].bpp, reverse=True)
        return frames[0][2]
    
    
    icon = Image.open("icon.ico")
    
    max_32 = get_max_bpp(icon)
    max_32.save("icon.png")
    
    

    So, a straight look at the .mode attribute of each frame won't help - all will show up as RGBA (at least for an ICO file which contains at least one 24 or 32bit frame. I had not tested with a file with indexed-only frames).