Image.open is generating a warning with icon files that contain PNG images
I'm posting this 'cause of a warning I've gotten when opening icon files, and what I did to get around the warning.
- OS: Windows 11 24H2 (OS Build 261000.4315) 10.0.26100 <- this was generated by python, and python could have miss reporting my actual version number 'cause of my heavily modified installation of windows.
- Python: 3.12.9
- Pillow: 11.1
An .ico file is a container designed to hold multiple images at different sizes and color depths (e.
- Historically, these embedded images were always simple bitmaps (DIBs).
- Today, you can place a compressed PNG image inside that container instead. This is highly recommended because PNGs offer excellent compression and full alpha transparency, resulting in smaller files and better-looking icons.
.ico
The StackOverflow.ico contains 256x256, 512x512, 768x768, and 1024x1024 PNG images, plus the typical bitmap images (16x16 20x20, 24x24, 32x32, etc.).
import io
import struct
import typing
import pathlib
from PIL import Image
# PIL\IcoImagePlugin.py:357: UserWarning: Image was not the expected size warnings.warn("Image was not the expected size")
icon_file = pathlib.Path(__file__).parent / 'StackOverflow.ico'
image = Image.open(str(icon_file))
# workaround
icon_data = icon_file.read_bytes()
count = struct.unpack("<H", icon_data[4:6])[0]
for i in range(count):
entry_offset = 6 + (16 * i)
if entry_offset + 16 > len(icon_data):
continue
length = struct.unpack("<I", icon_data[entry_offset + 8:entry_offset + 12])[0]
offset = struct.unpack("<I", icon_data[entry_offset + 12:entry_offset + 16])[0]
image: typing.Optional[Image.Image] = None
with io.BytesIO() as byte_stream:
image_content = icon_data[offset:offset + length]
if image_content.startswith(b'\x89PNG\r\n\x1a\n'):
byte_stream.write(image_content)
else:
byte_stream.write(b'\x00\x00\x01\x00')
byte_stream.write(struct.pack("<H", 1))
byte_stream.write(icon_data[entry_offset:entry_offset + 12])
byte_stream.write(struct.pack("<I", 22))
byte_stream.write(image_content)
byte_stream.seek(0)
image = Image.open(byte_stream)
image.load()
with image:
output_path = icon_file.parent / 'out' / f'icon_{image.width}x{image.height}.png'
output_path.parent.mkdir(parents = True, exist_ok = True)
image.save(fp = str(output_path), format = 'png' )
print(output_path.name)
Through trial and error, I was able to figure out that it was these bytes above the PNG images that is generating the warning . . .
byte_stream.write(b'\x00\x00\x01\x00')
byte_stream.write(struct.pack("<H", 1))
byte_stream.write(icon_data[entry_offset:entry_offset + 12])
byte_stream.write(struct.pack("<I", 22))
Hi. If you just posted this so that others can find it, ok, great, thanks for contributing to the overall pool of knowledge about Pillow.
If you'd like us to alter Pillow to better support these images, please provide an example file. Ideally, one that we could include in our test suite and distributed under the Pillow license.
For the moment, I downloaded the StackOverflow favicon, from https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico?v=ec617d715196, and was able to open and load it with Pillow without any warnings.
I couldn't attach the .ico file itself. The file type isn't supported, but I was able to zip it up and provide the zip file in my original post.
Thanks. Investigating, the size mismatch is because the header thinks the first image is (256, 256), but the first image is loaded by Pillow as (1024, 1024).
Converting to a PNG with ImageMagick, I get a (1024, 1024) image as the first frame.
Looking at through the code, the first image is set by https://github.com/python-pillow/Pillow/blob/a76dca9c459899a7022bde0fe04c35c36985690a/src/PIL/IcoImagePlugin.py#L160-L162
'See Wikipedia' would refer to https://en.wikipedia.org/wiki/ICO_(file_format)#Outline, where for the width and height,
Value 0 means image width is 256 pixels
So I think Pillow is operating correctly here, and the image is imperfect.
You are correct on 0x0 being a default to 256x256, and all of the PNG images are 0x0 in the icon files, regardless of their actual size. It is why I'm seeing this warning.
In my original post, I got around this warning by getting the bytes for just the PNG images themselves.
The Core Problem: A Single Byte
The original file format specification was created in the early days of Windows when icons were small (e..ico
- The
bWidthandbHeightfields in theICONDIRENTRYstructure were defined as being only one byte each. - A single unsigned byte can only hold values from 0 to 255.
- Therefore, it is physically impossible to store the number
256in thebWidthorbHeightfield.
The Solution: A Clever "Hack"
When Microsoft introduced support for large 256x256 icons in Windows Vista, they had to work around this limitation. Instead of creating a whole new file format and breaking every existing icon parser, they repurposed a value.
Since an icon with a width or height of 0 is nonsensical and would never exist, they established a new rule:
If the bWidth or bHeight field contains a 0, it should be interpreted as 256.
Here is a simple table illustrating the rule:
| ICONDIRENTRY bWidth / bHeight Value | Actual Dimension in Pixels |
|---|---|
| 1 to 255 | 1 to 255 |
| 0 | 256 |
So How Does a Program Know the Real Size?
This is why a robust icon parser cannot trust the ICONDIRENTRY for image dimensions when the format is PNG. The directory entry is merely a hint.
The correct parsing workflow is:
- Read the
ICONDIRENTRYfor an image. - See that
bWidthandbHeightare both0. This tells the parser two things:- The dimensions are probably 256x256.
- The image data is almost certainly in PNG format (as this convention was introduced along with PNG support).
- Jump to the
dwImageOffsetto find the start of the image data. - Completely ignore the
0x0dimensions from the directory. Instead, parse the image data itself to find the true dimensions. For a PNG, this means reading theIHDRchunk's width and height fields, which will correctly state1024x1024.
A lazy parser that trusts the 0x0 from the directory would fail, whereas a correct parser understands this special rule and knows to look inside the PNG stream for the ground truth.
It sounds to me like this is just a limitation of the format, conflicting with Pillow's model - we presume that you can read the header of an image and know the size.
https://github.com/python-pillow/Pillow/pull/7311#issuecomment-1723004045
For me this is one of the key concepts in Pillow: you can safely open the image file without decoding the whole image (which is much more expensive operation) and get any info from the file.
We do ultimately use the 'real size' after loading, and tell the user through a warning that the initial size was incorrect.