From 93b2185f06d2cc243abaf1e74e25f1c9848e9560 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Jul 2026 21:11:27 +1000 Subject: [PATCH 1/2] If image is smaller than (16, 16), save original size by default --- Tests/test_file_ico.py | 17 +++++++++++++---- docs/handbook/image-file-formats.rst | 4 ++-- src/PIL/IcoImagePlugin.py | 17 ++++++++++------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 36b608a0a98..1fbde6696dd 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -187,6 +187,15 @@ def test_incorrect_size() -> None: im.size = (1, 1) +def test_save_1x2(tmp_path: Path) -> None: + im = Image.new("1", (1, 2)) + outfile = tmp_path / "temp.ico" + im.save(outfile) + + with Image.open(outfile) as reloaded: + assert_image_equal(im, reloaded) + + def test_save_256x256(tmp_path: Path) -> None: """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange @@ -195,9 +204,9 @@ def test_save_256x256(tmp_path: Path) -> None: # Act im.save(outfile) - with Image.open(outfile) as im_saved: + with Image.open(outfile) as reloaded: # Assert - assert im_saved.size == (256, 256) + assert reloaded.size == (256, 256) def test_only_save_relevant_sizes(tmp_path: Path) -> None: @@ -211,9 +220,9 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None: # Act im.save(outfile) - with Image.open(outfile) as im_saved: + with Image.open(outfile) as reloaded: # Assert - assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} + assert reloaded.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} def test_save_append_images(tmp_path: Path) -> None: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 6b38b7278e8..7500f62a95a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -452,8 +452,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **sizes** A list of sizes included in this ico file; these are a 2-tuple, ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48), - (64, 64), (128, 128), (256, 256)]``. Any sizes bigger than the original - size or 256 will be ignored. + (64, 64), (128, 128), (256, 256)]``, or if it is smaller, only the image size. + Any sizes bigger than the original size or 256 will be ignored. The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 8dd57ff858a..97fa4858118 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -57,15 +57,18 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(_MAGIC) # (2+2) bmp = im.encoderinfo.get("bitmap_format") == "bmp" - sizes = im.encoderinfo.get( - "sizes", - [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], - ) + if "sizes" in im.encoderinfo: + sizes = sorted(set(im.encoderinfo["sizes"])) + else: + sizes = ( + [im.size] + if min(im.size) < 16 + else [(d, d) for d in (16, 24, 32, 48, 64, 128, 256)] + ) 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: + for size in sizes: + if size[0] > min(256, im.width) or size[1] > min(256, im.height): continue for provided_im in provided_ims: From f606d4e2d3eb895aaa164481edefb2cca700fc89 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Jul 2026 21:14:56 +1000 Subject: [PATCH 2/2] Raise ValueError if all sizes are too large for image --- Tests/test_file_ico.py | 5 +++++ src/PIL/IcoImagePlugin.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 1fbde6696dd..bf636f0a3dc 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -224,6 +224,11 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None: # Assert assert reloaded.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} + im2 = Image.new("1", (1, 1)) + outfile = tmp_path / "temp.ico" + with pytest.raises(ValueError, match="All sizes too large for image"): + im2.save(outfile, sizes=[(2, 2)]) + def test_save_append_images(tmp_path: Path) -> None: # append_images should be used for scaled down versions of the image diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 97fa4858118..0aa09476ffc 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -93,6 +93,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: frame = provided_im.copy() frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) frames.append(frame) + if not frames: + msg = "All sizes too large for image" + raise ValueError(msg) fp.write(o16(len(frames))) # idCount(2) offset = fp.tell() + len(frames) * 16 for frame in frames: