Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Tests/test_file_ico.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,43 @@ def test_save_to_bytes() -> None:
)


@pytest.mark.parametrize("size", ((1, 1), (8, 8), (15, 15), (8, 12)))
def test_save_smaller_than_default_sizes(size: tuple[int, int]) -> None:
# An image smaller than the smallest default size (16x16) must still be
# saved as a valid, readable ICO rather than a header-only, empty file.
im = Image.new("RGBA", size, (10, 20, 30, 255))
im.putpixel((size[0] - 1, size[1] - 1), (200, 100, 50, 255))

output = io.BytesIO()
im.save(output, "ico")

output.seek(0)
with Image.open(output) as reloaded:
assert reloaded.format == "ICO"
assert reloaded.size == size
assert reloaded.info["sizes"] == {size}
assert reloaded.convert("RGBA").getpixel((size[0] - 1, size[1] - 1)) == (
200,
100,
50,
255,
)


def test_save_all_sizes_larger_than_image() -> None:
# When every requested size is larger than the image, all are ignored, so
# fall back to the image's own size instead of writing an empty file.
im = Image.new("RGBA", (8, 8), (10, 20, 30, 255))

output = io.BytesIO()
im.save(output, "ico", sizes=[(16, 16), (32, 32)])

output.seek(0)
with Image.open(output) as reloaded:
assert reloaded.format == "ICO"
assert reloaded.size == (8, 8)


def test_getpixel(tmp_path: Path) -> None:
temp_file = tmp_path / "temp.ico"

Expand Down
16 changes: 12 additions & 4 deletions src/PIL/IcoImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,18 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
frames = []
provided_ims = [im] + im.encoderinfo.get("append_images", [])
width, height = im.size
for size in sorted(set(sizes)):
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
continue

sizes = [
size
for size in sorted(set(sizes))
if size[0] <= width and size[1] <= height and size[0] <= 256 and size[1] <= 256
]
if not sizes:
# Every requested size is larger than the source image, so fall back to
# the image's own size (capped at the 256x256 ICO maximum). This avoids
# writing an empty, unreadable file when the image is smaller than the
# smallest default size.
sizes = [(min(width, 256), min(height, 256))]
for size in sizes:
for provided_im in provided_ims:
if provided_im.size != size:
continue
Expand Down
Loading