diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 36b608a0a98..e8f94b09d0d 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -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" diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 8dd57ff858a..32cabeedfcf 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -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